diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java index a53352d..6d4d2e3 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponent.java @@ -148,6 +148,65 @@ public class SteadyChecksquareInfluxQueryComponent { return result; } + public Map> queryStatValuePointMap(List fields, + LocalDateTime startTime, + LocalDateTime endTime, + int intervalMinutes) { + Map> result = + new LinkedHashMap>(); + if (fields == null || fields.isEmpty()) { + return result; + } + if (!hasValueTypeTag(fields.get(0).getMeasurement())) { + for (SteadyTrendResolvedFieldBO field : fields) { + result.put(resolveValueType(field.getStatType()), queryValuePoints(field, startTime, endTime, intervalMinutes)); + } + return result; + } + validateConfig(); + Map> cache = REQUEST_VALUE_CACHE.get(); + List missingFields = new ArrayList(); + for (SteadyTrendResolvedFieldBO field : fields) { + String statType = resolveValueType(field.getStatType()); + String cacheKey = buildCacheKey(buildValuePointQuery(field, startTime, endTime), intervalMinutes); + if (cache != null && cache.containsKey(cacheKey)) { + result.put(statType, new ArrayList(cache.get(cacheKey))); + } else { + missingFields.add(field); + } + } + if (!missingFields.isEmpty()) { + String query = buildStatValuePointQuery(missingFields, startTime, endTime); + long startMillis = System.currentTimeMillis(); + SteadyTrendResolvedFieldBO first = missingFields.get(0); + log.info("数据校验指标值 InfluxDB 统计类型批量查询开始,measurement={},field={},statCount={},lineId={},phase={},query={}", + first.getMeasurement(), first.getField(), missingFields.size(), first.getLineId(), first.getPhase(), query); + try { + Map> queried = + queryStatValuePointsByWindow(missingFields, startTime, endTime, intervalMinutes); + for (SteadyTrendResolvedFieldBO field : missingFields) { + String statType = resolveValueType(field.getStatType()); + List points = queried.get(statType); + if (points == null) { + points = new ArrayList(); + } + result.put(statType, points); + if (cache != null) { + String cacheKey = buildCacheKey(buildValuePointQuery(field, startTime, endTime), intervalMinutes); + cache.put(cacheKey, new ArrayList(points)); + } + } + log.info("数据校验指标值 InfluxDB 统计类型批量查询结束,statCount={},costMs={}", + missingFields.size(), System.currentTimeMillis() - startMillis); + } catch (RuntimeException ex) { + log.warn("数据校验指标值 InfluxDB 统计类型批量查询异常,statCount={},costMs={},error={}", + missingFields.size(), System.currentTimeMillis() - startMillis, ex.getMessage()); + throw ex; + } + } + return result; + } + public String buildChecksquareQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime) { return buildValuePointQuery(field, startTime, endTime); } @@ -197,6 +256,33 @@ public class SteadyChecksquareInfluxQueryComponent { return result; } + private Map> queryStatValuePointsByWindow(List fields, + LocalDateTime startTime, + LocalDateTime endTime, + int intervalMinutes) { + Map> result = + new LinkedHashMap>(); + for (SteadyTrendResolvedFieldBO field : fields) { + result.put(resolveValueType(field.getStatType()), new ArrayList()); + } + LocalDateTime windowStart = startTime; + while (!windowStart.isAfter(endTime)) { + LocalDateTime windowEnd = min(windowStart.plusDays(QUERY_WINDOW_DAYS).minusNanos(1), endTime); + Map> windowResult = + parseStatValuePoints(executeQuery(buildStatValuePointQuery(fields, windowStart, windowEnd)), intervalMinutes); + for (Map.Entry> entry : windowResult.entrySet()) { + List points = result.get(entry.getKey()); + if (points == null) { + points = new ArrayList(); + result.put(entry.getKey(), points); + } + points.addAll(entry.getValue()); + } + windowStart = windowEnd.plusNanos(1); + } + return result; + } + private LocalDateTime min(LocalDateTime first, LocalDateTime second) { return first.isAfter(second) ? second : first; } @@ -238,6 +324,28 @@ public class SteadyChecksquareInfluxQueryComponent { return sql.toString(); } + public String buildStatValuePointQuery(List fields, LocalDateTime startTime, LocalDateTime endTime) { + SteadyTrendResolvedFieldBO first = fields.get(0); + StringBuilder sql = new StringBuilder(); + sql.append("SELECT \"").append(first.getField()).append("\" AS \"value\""); + sql.append(" FROM \"").append(first.getMeasurement()).append("\""); + sql.append(" WHERE time >= '").append(INFLUX_TIME_FORMATTER.format(startTime)).append("'"); + sql.append(" AND time <= '").append(INFLUX_TIME_FORMATTER.format(endTime)).append("'"); + sql.append(" AND \"line_id\" = '").append(escapeTagValue(first.getLineId())).append("'"); + sql.append(" AND \"phasic_type\" = '").append(escapeTagValue(first.getPhase())).append("'"); + sql.append(" AND \"value_type\" =~ /^("); + for (int i = 0; i < fields.size(); i++) { + if (i > 0) { + sql.append("|"); + } + sql.append(escapeRegexValue(resolveValueType(fields.get(i).getStatType()))); + } + sql.append(")$/"); + sql.append(" GROUP BY \"value_type\""); + sql.append(" ORDER BY time ASC"); + return sql.toString(); + } + private List parseValuePoints(String body, int intervalMinutes) { try { JsonNode root = OBJECT_MAPPER.readTree(body); @@ -312,6 +420,52 @@ public class SteadyChecksquareInfluxQueryComponent { } } + private Map> parseStatValuePoints(String body, int intervalMinutes) { + try { + JsonNode root = OBJECT_MAPPER.readTree(body); + JsonNode seriesArray = root.path("results").path(0).path("series"); + Map> result = + new LinkedHashMap>(); + if (!seriesArray.isArray()) { + return result; + } + for (JsonNode series : seriesArray) { + String statType = series.path("tags").path("value_type").asText(null); + if (statType == null || statType.trim().isEmpty()) { + continue; + } + statType = resolveValueType(statType); + List points = result.get(statType); + if (points == null) { + points = new ArrayList(); + result.put(statType, points); + } + JsonNode values = series.path("values"); + if (!values.isArray()) { + continue; + } + for (JsonNode value : values) { + if (value.size() < 2 || value.get(1).isNull()) { + continue; + } + LocalDateTime time = parseInfluxTime(value.get(0).asText()); + if (time == null) { + continue; + } + SteadyChecksquareValuePointBO point = new SteadyChecksquareValuePointBO(); + point.setTime(alignToPreviousSlot(time, intervalMinutes)); + point.setValue(new BigDecimal(value.get(1).asText())); + points.add(point); + } + } + return result; + } catch (IOException ex) { + throw fail("InfluxDB 返回结果解析失败:" + ex.getMessage()); + } catch (NumberFormatException ex) { + throw fail("InfluxDB 返回指标值格式不正确:" + ex.getMessage()); + } + } + private LocalDateTime alignToPreviousSlot(LocalDateTime time, int intervalMinutes) { LocalDateTime minuteFloor = time.withSecond(0).withNano(0); int minuteOfDay = minuteFloor.getHour() * 60 + minuteFloor.getMinute(); @@ -390,6 +544,16 @@ public class SteadyChecksquareInfluxQueryComponent { return value == null ? "" : value.replace("\\", "\\\\").replace("'", "\\'"); } + private String escapeRegexValue(String value) { + return value == null ? "" : value.replace("\\", "\\\\") + .replace("|", "\\|") + .replace("(", "\\(") + .replace(")", "\\)") + .replace("^", "\\^") + .replace("$", "\\$") + .replace(".", "\\."); + } + private String resolveValueType(String statType) { if (statType == null || statType.trim().isEmpty()) { return "AVG"; diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java index 1555a59..8d5bffa 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponent.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; @@ -58,9 +59,18 @@ public class SteadyChecksquareValueOrderRuleComponent { LocalDateTime startTime, LocalDateTime endTime, int intervalMinutes) { Map> result = new LinkedHashMap>(); + List fields = new ArrayList(); for (String statType : REQUIRED_STATS) { SteadyTrendResolvedFieldBO field = buildResolvedField(lineId, indicator, harmonicOrder, phase, statType); - result.put(statType, toValueMap(influxQueryComponent.queryValuePoints(field, startTime, endTime, intervalMinutes))); + fields.add(field); + } + Map> fieldValueMap = + influxQueryComponent.queryStatValuePointMap(fields, startTime, endTime, intervalMinutes); + if (fieldValueMap == null) { + fieldValueMap = Collections.emptyMap(); + } + for (String statType : REQUIRED_STATS) { + result.put(statType, toValueMap(fieldValueMap.get(statType))); } return result; } diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/config/SteadyChecksquareExecutorConfig.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/config/SteadyChecksquareExecutorConfig.java new file mode 100644 index 0000000..b48d3b6 --- /dev/null +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/config/SteadyChecksquareExecutorConfig.java @@ -0,0 +1,41 @@ +package com.njcn.gather.steady.checksquare.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 数据校验后台任务线程池配置。 + */ +@Slf4j +@Configuration +public class SteadyChecksquareExecutorConfig { + + @Bean(name = "steadyChecksquareExecutorService", destroyMethod = "shutdown") + public ExecutorService steadyChecksquareExecutorService() { + AtomicInteger threadIndex = new AtomicInteger(1); + return new ThreadPoolExecutor( + 1, + 1, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue(8), + runnable -> { + Thread thread = new Thread(runnable); + thread.setName("steady-checksquare-task-" + threadIndex.getAndIncrement()); + return thread; + }, + (runnable, executor) -> { + log.warn("数据校验任务线程池已满,拒绝新的校验任务"); + throw new RejectedExecutionException("数据校验任务线程池已满"); + } + ); + } +} diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareController.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareController.java index 532180c..c38b1ac 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareController.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareController.java @@ -61,6 +61,16 @@ public class SteadyChecksquareController extends BaseController { return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); } + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("Restart failed checksquare task") + @PostMapping("/restart") + public HttpResult restart(@RequestParam("taskId") String taskId) { + String methodDescribe = getMethodDescribe("restart"); + LogUtil.njcnDebug(log, "{} restart checksquare task, taskId={}", methodDescribe, taskId); + SteadyChecksquareTaskVO result = checksquareService.restart(taskId); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE) @ApiOperation("删除数据校验任务") @PostMapping("/delete") diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareQueryParam.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareQueryParam.java index 611ac57..7fc6b4e 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareQueryParam.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/param/SteadyChecksquareQueryParam.java @@ -19,6 +19,9 @@ public class SteadyChecksquareQueryParam implements Serializable { @ApiModelProperty("监测点 ID") private String lineId; + @ApiModelProperty("监测点 ID 列表") + private List lineIds; + @ApiModelProperty("指标编码") private List indicatorCodes; diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareItemPO.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareItemPO.java index 5612589..e01950e 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareItemPO.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareItemPO.java @@ -24,6 +24,10 @@ public class SteadyChecksquareItemPO implements Serializable { private String taskId; @TableField("item_key") private String itemKey; + @TableField("line_id") + private String lineId; + @TableField("line_name") + private String lineName; @TableField("indicator_code") private String indicatorCode; @TableField("indicator_name") diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareTaskPO.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareTaskPO.java index 06c25f9..fb808cf 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareTaskPO.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/po/SteadyChecksquareTaskPO.java @@ -26,6 +26,10 @@ public class SteadyChecksquareTaskPO implements Serializable { private String lineId; @TableField("line_name") private String lineName; + @TableField("line_ids_json") + private String lineIdsJson; + @TableField("line_ids_text") + private String lineIdsText; @TableField("time_start") private LocalDateTime timeStart; @TableField("time_end") diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemDetailVO.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemDetailVO.java index ee86da2..354f7bf 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemDetailVO.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemDetailVO.java @@ -20,6 +20,12 @@ public class SteadyChecksquareItemDetailVO implements Serializable { @ApiModelProperty("检测项 ID") private String itemId; + @ApiModelProperty("监测点 ID") + private String lineId; + + @ApiModelProperty("监测点名称") + private String lineName; + @ApiModelProperty("明细类型") private String detailType; diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java index c1c11c4..bf12efd 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareItemVO.java @@ -24,6 +24,12 @@ public class SteadyChecksquareItemVO implements Serializable { @ApiModelProperty("校验项唯一键") private String itemKey; + @ApiModelProperty("监测点 ID") + private String lineId; + + @ApiModelProperty("监测点名称") + private String lineName; + @ApiModelProperty("指标编码") private String indicatorCode; diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareQueryVO.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareQueryVO.java index b6cac48..b325863 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareQueryVO.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareQueryVO.java @@ -26,6 +26,9 @@ public class SteadyChecksquareQueryVO implements Serializable { @ApiModelProperty("监测点 ID") private String lineId; + @ApiModelProperty("监测点 ID 列表") + private List lineIds = new ArrayList(); + @ApiModelProperty("监测点名称") private String lineName; diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareTaskVO.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareTaskVO.java index 571e979..8506d95 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareTaskVO.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/pojo/vo/SteadyChecksquareTaskVO.java @@ -6,6 +6,7 @@ import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; +import java.util.List; /** * 数据校验历史任务。 @@ -25,6 +26,9 @@ public class SteadyChecksquareTaskVO implements Serializable { @ApiModelProperty("监测点 ID") private String lineId; + @ApiModelProperty("监测点 ID 列表") + private List lineIds; + @ApiModelProperty("监测点名称") private String lineName; diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareService.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareService.java index 2857a97..9a25b0c 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareService.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/SteadyChecksquareService.java @@ -18,6 +18,8 @@ public interface SteadyChecksquareService { SteadyChecksquareTaskVO create(SteadyChecksquareQueryParam param); + SteadyChecksquareTaskVO restart(String taskId); + boolean delete(List taskIds); SteadyChecksquareQueryVO detail(String taskId); diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java index d7d437a..9dd1b8d 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java @@ -46,6 +46,7 @@ import com.njcn.web.factory.PageFactory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionTemplate; @@ -64,6 +65,8 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; import java.util.stream.Collectors; /** @@ -98,12 +101,16 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { @Autowired(required = false) private TransactionTemplate transactionTemplate; + @Autowired(required = false) + @Qualifier("steadyChecksquareExecutorService") + private ExecutorService steadyChecksquareExecutorService; + @Override public Page query(SteadyChecksquareHistoryQueryParam param) { SteadyChecksquareHistoryQueryParam query = param == null ? new SteadyChecksquareHistoryQueryParam() : param; LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(SteadyChecksquareTaskPO::getState, SteadyChecksquareConst.STATE_ENABLED) - .eq(trimToNull(query.getLineId()) != null, SteadyChecksquareTaskPO::getLineId, trimToNull(query.getLineId())) + .like(trimToNull(query.getLineId()) != null, SteadyChecksquareTaskPO::getLineIdsText, "|" + trimToNull(query.getLineId()) + "|") .like(trimToNull(query.getIndicatorCode()) != null, SteadyChecksquareTaskPO::getIndicatorCodesText, "|" + trimToNull(query.getIndicatorCode()) + "|") .ge(trimToNull(query.getTimeStart()) != null, SteadyChecksquareTaskPO::getTimeStart, parseOptionalTime(query.getTimeStart())) .le(trimToNull(query.getTimeEnd()) != null, SteadyChecksquareTaskPO::getTimeEnd, parseOptionalTime(query.getTimeEnd())) @@ -118,24 +125,47 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { @Override public SteadyChecksquareTaskVO create(SteadyChecksquareQueryParam param) { + long createStartMillis = System.currentTimeMillis(); validateCreateBaseParam(param); - String lineId = trimToNull(param.getLineId()); + List lineIds = resolveLineIds(param); LocalDateTime startTime = parseRequiredTime(param.getTimeStart(), "开始时间不能为空"); LocalDateTime endTime = parseRequiredTime(param.getTimeEnd(), "结束时间不能为空"); - SteadyChecksquareTaskPO existedTask = findExistingTask(lineId, startTime, endTime); + SteadyChecksquareTaskPO existedTask = findExistingTask(lineIds, startTime, endTime); if (existedTask != null) { + log.info("数据校验创建命中已有任务,taskId={},lineId={},costMs={}", + existedTask.getId(), lineIds, System.currentTimeMillis() - createStartMillis); return toTaskVO(existedTask); } - prepareCreateContext(param); - influxQueryComponent.enableRequestCache(); + CreateContext context = prepareCreateContext(param); + SteadyChecksquareQueryParam taskParam = copyCreateParam(param, context.indicatorCodes); + SteadyChecksquareTaskPO task = saveRunningTaskInTransaction(taskParam, context); try { - SteadyChecksquareQueryVO result = calculate(param); - SteadyChecksquareTaskPO task = saveResultInTransaction(param, result); - return toTaskVO(task); - } finally { - influxQueryComponent.clearRequestCache(); + submitCreateTask(task.getId(), taskParam); + } catch (RejectedExecutionException ex) { + markTaskFail(task.getId(), ex.getMessage()); + throw fail("数据校验任务线程池已满,请稍后重试"); } + log.info("数据校验创建任务已提交后台执行,taskId={},lineId={},indicatorCount={},costMs={}", + task.getId(), context.lineIds, context.indicatorCodes.size(), System.currentTimeMillis() - createStartMillis); + return toTaskVO(task); } + @Override + public SteadyChecksquareTaskVO restart(String taskId) { + SteadyChecksquareTaskPO task = requireTask(taskId); + if (!SteadyChecksquareConst.TASK_STATUS_FAIL.equals(task.getTaskStatus())) { + throw fail("只有执行失败的数据校验任务允许重新启动"); + } + SteadyChecksquareQueryParam taskParam = buildRestartParam(task); + SteadyChecksquareTaskPO runningTask = resetFailTaskInTransaction(task); + try { + submitCreateTask(runningTask.getId(), taskParam); + } catch (RejectedExecutionException ex) { + markTaskFail(runningTask.getId(), ex.getMessage()); + throw fail("数据校验任务线程池已满,请稍后重试"); + } + return toTaskVO(runningTask); + } + @Override public boolean delete(List taskIds) { List ids = normalizeTextList(taskIds); @@ -166,6 +196,7 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { result.setTaskId(task.getId()); result.setTaskNo(task.getTaskNo()); result.setLineId(task.getLineId()); + result.setLineIds(readTaskLineIds(task)); result.setLineName(task.getLineName()); result.setTimeStart(formatTime(task.getTimeStart())); result.setTimeEnd(formatTime(task.getTimeEnd())); @@ -198,6 +229,8 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { } SteadyChecksquareItemDetailVO result = new SteadyChecksquareItemDetailVO(); result.setItemId(item.getId()); + result.setLineId(item.getLineId()); + result.setLineName(item.getLineName()); result.setDetailType(type); result.setStatType(statType); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() @@ -240,22 +273,85 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { return taskResult; } + private SteadyChecksquareTaskPO resetFailTaskInTransaction(SteadyChecksquareTaskPO task) { + if (transactionTemplate == null) { + return resetFailTask(task); + } + return transactionTemplate.execute(status -> resetFailTask(task)); + } + + private SteadyChecksquareTaskPO resetFailTask(SteadyChecksquareTaskPO task) { + LocalDateTime now = LocalDateTime.now(); + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .set(SteadyChecksquareTaskPO::getTaskStatus, SteadyChecksquareConst.TASK_STATUS_RUNNING) + .set(SteadyChecksquareTaskPO::getItemCount, 0) + .set(SteadyChecksquareTaskPO::getAbnormalItemCount, 0) + .set(SteadyChecksquareTaskPO::getMinDataIntegrity, BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP)) + .set(SteadyChecksquareTaskPO::getResultMessage, "数据校验任务重新执行中") + .set(SteadyChecksquareTaskPO::getUpdateTime, now) + .eq(SteadyChecksquareTaskPO::getId, task.getId()) + .eq(SteadyChecksquareTaskPO::getTaskStatus, SteadyChecksquareConst.TASK_STATUS_FAIL) + .eq(SteadyChecksquareTaskPO::getState, SteadyChecksquareConst.STATE_ENABLED); + if (!taskService.update(wrapper)) { + throw fail("数据校验任务状态已变化,请刷新后重试"); + } + clearTaskResults(task.getId()); + task.setTaskStatus(SteadyChecksquareConst.TASK_STATUS_RUNNING); + task.setItemCount(0); + task.setAbnormalItemCount(0); + task.setMinDataIntegrity(BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP)); + task.setResultMessage("数据校验任务重新执行中"); + task.setUpdateTime(now); + return task; + } + + private void clearTaskResults(String taskId) { + List items = itemService.lambdaQuery() + .eq(SteadyChecksquareItemPO::getTaskId, taskId) + .list(); + if (items == null || items.isEmpty()) { + return; + } + List itemIds = items.stream().map(SteadyChecksquareItemPO::getId).collect(Collectors.toList()); + detailService.remove(new LambdaQueryWrapper() + .in(SteadyChecksquareDetailPO::getItemId, itemIds)); + statSummaryService.remove(new LambdaQueryWrapper() + .in(SteadyChecksquareStatSummaryPO::getItemId, itemIds)); + itemService.remove(new LambdaQueryWrapper() + .eq(SteadyChecksquareItemPO::getTaskId, taskId)); + } + + private SteadyChecksquareQueryParam buildRestartParam(SteadyChecksquareTaskPO task) { + List lineIds = readTaskLineIds(task); + List indicatorCodes = readStringList(task.getIndicatorCodesJson()); + if (indicatorCodes.isEmpty()) { + indicatorCodes = parseTextListSearchValue(task.getIndicatorCodesText()); + } + SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam(); + param.setLineId(lineIds.isEmpty() ? task.getLineId() : lineIds.get(0)); + param.setLineIds(lineIds); + param.setIndicatorCodes(indicatorCodes); + param.setTimeStart(formatTime(task.getTimeStart())); + param.setTimeEnd(formatTime(task.getTimeEnd())); + return param; + } + private CreateContext prepareCreateContext(SteadyChecksquareQueryParam param) { validateParam(param); - String lineId = trimToNull(param.getLineId()); + List lineIds = resolveLineIds(param); LocalDateTime startTime = parseRequiredTime(param.getTimeStart(), "开始时间不能为空"); LocalDateTime endTime = parseRequiredTime(param.getTimeEnd(), "结束时间不能为空"); if (startTime.isAfter(endTime)) { throw fail("开始时间不能大于结束时间"); } - AddLedgerLinePathVO linePath = requireLinePath(lineId); - int intervalMinutes = resolveIntervalMinutes(linePath); - List indicatorCodes = normalizeTextList(param.getIndicatorCodes()); + Map linePathMap = requireLinePaths(lineIds); + int intervalMinutes = resolveIntervalMinutes(linePathMap.get(lineIds.get(0))); + List indicatorCodes = resolveIndicatorCodes(param); for (String indicatorCode : indicatorCodes) { requireIndicator(indicatorCode); } validateCreateTimeRange(startTime, endTime); - return new CreateContext(lineId, linePath, startTime, endTime, intervalMinutes, indicatorCodes); + return new CreateContext(lineIds, linePathMap, startTime, endTime, intervalMinutes, indicatorCodes); } private void validateCreateTimeRange(LocalDateTime startTime, LocalDateTime endTime) { @@ -264,10 +360,10 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { } } - private SteadyChecksquareTaskPO findExistingTask(String lineId, LocalDateTime startTime, LocalDateTime endTime) { + private SteadyChecksquareTaskPO findExistingTask(List lineIds, LocalDateTime startTime, LocalDateTime endTime) { List tasks = taskService.lambdaQuery() .eq(SteadyChecksquareTaskPO::getState, SteadyChecksquareConst.STATE_ENABLED) - .eq(SteadyChecksquareTaskPO::getLineId, lineId) + .eq(SteadyChecksquareTaskPO::getLineIdsText, buildTextListSearchValue(lineIds)) .eq(SteadyChecksquareTaskPO::getTimeStart, startTime) .eq(SteadyChecksquareTaskPO::getTimeEnd, endTime) .orderByDesc(SteadyChecksquareTaskPO::getCreateTime) @@ -287,36 +383,43 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { private SteadyChecksquareQueryVO calculate(SteadyChecksquareQueryParam param) { validateParam(param); - String lineId = trimToNull(param.getLineId()); + List lineIds = resolveLineIds(param); LocalDateTime startTime = parseRequiredTime(param.getTimeStart(), "开始时间不能为空"); LocalDateTime endTime = parseRequiredTime(param.getTimeEnd(), "结束时间不能为空"); if (startTime.isAfter(endTime)) { throw fail("开始时间不能大于结束时间"); } - AddLedgerLinePathVO linePath = requireLinePath(lineId); - int intervalMinutes = resolveIntervalMinutes(linePath); + Map linePathMap = requireLinePaths(lineIds); + int intervalMinutes = resolveIntervalMinutes(linePathMap.get(lineIds.get(0))); SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO(); - result.setLineId(lineId); - result.setLineName(trimToNull(linePath.getLineName()) == null ? EMPTY_TEXT : linePath.getLineName()); + result.setLineId(lineIds.get(0)); + result.setLineIds(new ArrayList(lineIds)); + result.setLineName(buildLineNames(lineIds, linePathMap)); result.setTimeStart(param.getTimeStart()); result.setTimeEnd(param.getTimeEnd()); result.setIntervalMinutes(intervalMinutes); long startMillis = System.currentTimeMillis(); - List indicatorCodes = normalizeTextList(param.getIndicatorCodes()); + List indicatorCodes = resolveIndicatorCodes(param); List indicators = new ArrayList(); for (String indicatorCode : indicatorCodes) { indicators.add(requireIndicator(indicatorCode)); } log.info("数据校验新增检测开始,lineId={},indicatorCount={},timeStart={},timeEnd={},intervalMinutes={}", - lineId, indicatorCodes.size(), startTime, endTime, intervalMinutes); - prefetchNormalIndicatorPoints(lineId, indicators, startTime, endTime, intervalMinutes); - for (SteadyTrendIndicatorDefinitionBO indicator : indicators) { - int itemIntervalMinutes = resolveIndicatorIntervalMinutes(indicator, intervalMinutes); - List itemSlots = timeSlotCalculator.buildTimeSlots(startTime, endTime, itemIntervalMinutes); - result.getItems().addAll(buildIndicatorItems(lineId, indicator, startTime, endTime, itemSlots, itemIntervalMinutes)); + lineIds, indicatorCodes.size(), startTime, endTime, intervalMinutes); + for (String lineId : lineIds) { + AddLedgerLinePathVO linePath = linePathMap.get(lineId); + int lineIntervalMinutes = resolveIntervalMinutes(linePath); + String lineName = linePath == null ? null : trimToNull(linePath.getLineName()); + prefetchNormalIndicatorPoints(lineId, indicators, startTime, endTime, lineIntervalMinutes); + for (SteadyTrendIndicatorDefinitionBO indicator : indicators) { + int itemIntervalMinutes = resolveIndicatorIntervalMinutes(indicator, lineIntervalMinutes); + List itemSlots = timeSlotCalculator.buildTimeSlots(startTime, endTime, itemIntervalMinutes); + result.getItems().addAll(buildIndicatorItems(lineId, lineName, indicator, startTime, endTime, + itemSlots, itemIntervalMinutes)); + } } - log.info("数据校验新增检测结束,lineId={},itemCount={},costMs={}", lineId, result.getItems().size(), System.currentTimeMillis() - startMillis); + log.info("数据校验新增检测结束,lineIds={},itemCount={},costMs={}", lineIds, result.getItems().size(), System.currentTimeMillis() - startMillis); return result; } @@ -327,6 +430,13 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { return transactionTemplate.execute(status -> saveResult(param, result)); } + private SteadyChecksquareTaskPO saveRunningTaskInTransaction(SteadyChecksquareQueryParam param, CreateContext context) { + if (transactionTemplate == null) { + return saveRunningTask(param, context); + } + return transactionTemplate.execute(status -> saveRunningTask(param, context)); + } + private SteadyChecksquareTaskPO saveResultInTransaction(String taskId, SteadyChecksquareQueryParam param, SteadyChecksquareQueryVO result) { if (transactionTemplate == null) { return saveResult(taskId, param, result); @@ -338,17 +448,73 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { return saveResult(null, param, result); } + private SteadyChecksquareTaskPO saveRunningTask(SteadyChecksquareQueryParam param, CreateContext context) { + LocalDateTime now = LocalDateTime.now(); + SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO(); + task.setId(SteadyChecksquareIdUtil.uuid()); + task.setTaskNo(SteadyChecksquareIdUtil.taskNo()); + task.setLineId(context.lineIds.get(0)); + task.setLineName(buildLineNames(context.lineIds, context.linePathMap)); + task.setLineIdsJson(writeJson(context.lineIds)); + task.setLineIdsText(buildTextListSearchValue(context.lineIds)); + task.setTimeStart(context.startTime); + task.setTimeEnd(context.endTime); + task.setIntervalMinutes(context.intervalMinutes); + task.setIndicatorCodesJson(writeJson(context.indicatorCodes)); + task.setIndicatorCodesText(buildIndicatorCodesText(context.indicatorCodes)); + task.setTaskStatus(SteadyChecksquareConst.TASK_STATUS_RUNNING); + task.setItemCount(0); + task.setAbnormalItemCount(0); + task.setMinDataIntegrity(BigDecimal.ZERO.setScale(6, RoundingMode.HALF_UP)); + task.setResultMessage("数据校验任务执行中"); + task.setState(SteadyChecksquareConst.STATE_ENABLED); + task.setCreateTime(now); + task.setUpdateTime(now); + taskService.save(task); + return task; + } + + private void submitCreateTask(String taskId, SteadyChecksquareQueryParam param) { + if (steadyChecksquareExecutorService == null) { + executeCreateTask(taskId, param); + return; + } + steadyChecksquareExecutorService.submit(() -> executeCreateTask(taskId, param)); + } + + private void executeCreateTask(String taskId, SteadyChecksquareQueryParam param) { + long startMillis = System.currentTimeMillis(); + influxQueryComponent.enableRequestCache(); + try { + SteadyChecksquareQueryVO result = calculate(param); + SteadyChecksquareTaskPO task = saveResultInTransaction(taskId, param, result); + log.info("数据校验后台任务执行成功,taskId={},itemCount={},costMs={}", + taskId, task.getItemCount(), System.currentTimeMillis() - startMillis); + } catch (Exception ex) { + log.error("数据校验后台任务执行失败,taskId={},costMs={}", taskId, System.currentTimeMillis() - startMillis, ex); + markTaskFail(taskId, ex.getMessage()); + } finally { + influxQueryComponent.clearRequestCache(); + } + } + private SteadyChecksquareTaskPO saveResult(String taskId, SteadyChecksquareQueryParam param, SteadyChecksquareQueryVO result) { + long saveStartMillis = System.currentTimeMillis(); LocalDateTime now = LocalDateTime.now(); SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO(); task.setId(trimToNull(taskId) == null ? SteadyChecksquareIdUtil.uuid() : taskId); - task.setTaskNo(SteadyChecksquareIdUtil.taskNo()); + if (trimToNull(taskId) == null) { + task.setTaskNo(SteadyChecksquareIdUtil.taskNo()); + } task.setLineId(result.getLineId()); task.setLineName(result.getLineName()); + List lineIds = resolveResultLineIds(result); + task.setLineIdsJson(writeJson(lineIds)); + task.setLineIdsText(buildTextListSearchValue(lineIds)); task.setTimeStart(parseRequiredTime(result.getTimeStart(), "开始时间不能为空")); task.setTimeEnd(parseRequiredTime(result.getTimeEnd(), "结束时间不能为空")); task.setIntervalMinutes(result.getIntervalMinutes()); - List indicatorCodes = normalizeTextList(param.getIndicatorCodes()); + List indicatorCodes = resolveIndicatorCodes(param); task.setIndicatorCodesJson(writeJson(indicatorCodes)); task.setIndicatorCodesText(buildIndicatorCodesText(indicatorCodes)); task.setTaskStatus(SteadyChecksquareConst.TASK_STATUS_SUCCESS); @@ -384,12 +550,18 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { if (!detailPOs.isEmpty()) { saveDetailBatchInChunks(detailPOs); } + log.info("数据校验结果保存完成,taskId={},itemCount={},summaryCount={},detailCount={},costMs={}", + task.getId(), itemPOs.size(), summaryPOs.size(), detailPOs.size(), System.currentTimeMillis() - saveStartMillis); return task; } private void updateCompletedTask(SteadyChecksquareTaskPO task) { LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() .set(SteadyChecksquareTaskPO::getLineName, task.getLineName()) + .set(SteadyChecksquareTaskPO::getLineIdsJson, task.getLineIdsJson()) + .set(SteadyChecksquareTaskPO::getLineIdsText, task.getLineIdsText()) + .set(SteadyChecksquareTaskPO::getIndicatorCodesJson, task.getIndicatorCodesJson()) + .set(SteadyChecksquareTaskPO::getIndicatorCodesText, task.getIndicatorCodesText()) .set(SteadyChecksquareTaskPO::getIntervalMinutes, task.getIntervalMinutes()) .set(SteadyChecksquareTaskPO::getTaskStatus, task.getTaskStatus()) .set(SteadyChecksquareTaskPO::getItemCount, task.getItemCount()) @@ -414,6 +586,8 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { po.setId(SteadyChecksquareIdUtil.uuid()); po.setTaskId(taskId); po.setItemKey(item.getItemKey()); + po.setLineId(item.getLineId()); + po.setLineName(item.getLineName()); po.setIndicatorCode(item.getIndicatorCode()); po.setIndicatorName(item.getIndicatorName()); po.setHarmonicOrder(item.getHarmonicOrder()); @@ -542,7 +716,8 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { + field.getStatType() + "|" + intervalMinutes; } - private List buildIndicatorItems(String lineId, SteadyTrendIndicatorDefinitionBO indicator, + private List buildIndicatorItems(String lineId, String lineName, + SteadyTrendIndicatorDefinitionBO indicator, LocalDateTime startTime, LocalDateTime endTime, List slots, int intervalMinutes) { List result = new ArrayList(); @@ -550,12 +725,12 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { List harmonicOrders = buildAggregateHarmonicOrders(indicator); prefetchHarmonicIndicatorPoints(lineId, indicator, harmonicOrders, startTime, endTime, intervalMinutes); for (Integer order : harmonicOrders) { - result.add(buildItem(lineId, indicator, order, startTime, endTime, slots, intervalMinutes)); + result.add(buildItem(lineId, lineName, indicator, order, startTime, endTime, slots, intervalMinutes)); } fillHarmonicParityRuleResult(result, lineId, indicator, startTime, endTime, intervalMinutes); - return Collections.singletonList(aggregateHarmonicItems(lineId, indicator, result, intervalMinutes)); + return Collections.singletonList(aggregateHarmonicItems(lineId, lineName, indicator, result, intervalMinutes)); } - result.add(buildItem(lineId, indicator, null, startTime, endTime, slots, intervalMinutes)); + result.add(buildItem(lineId, lineName, indicator, null, startTime, endTime, slots, intervalMinutes)); return result; } @@ -576,10 +751,13 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { } } - private SteadyChecksquareItemVO aggregateHarmonicItems(String lineId, SteadyTrendIndicatorDefinitionBO indicator, + private SteadyChecksquareItemVO aggregateHarmonicItems(String lineId, String lineName, + SteadyTrendIndicatorDefinitionBO indicator, List orderItems, int intervalMinutes) { SteadyChecksquareItemVO result = new SteadyChecksquareItemVO(); result.setItemKey(buildItemKey(lineId, indicator, null)); + result.setLineId(lineId); + result.setLineName(lineName); result.setIndicatorCode(indicator.getIndicatorCode()); result.setIndicatorName(indicator.getName()); result.setHarmonicOrder(null); @@ -654,11 +832,14 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { return new ArrayList(detailMap.values()); } - private SteadyChecksquareItemVO buildItem(String lineId, SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder, + private SteadyChecksquareItemVO buildItem(String lineId, String lineName, + SteadyTrendIndicatorDefinitionBO indicator, Integer harmonicOrder, LocalDateTime startTime, LocalDateTime endTime, List slots, int intervalMinutes) { SteadyChecksquareItemVO item = new SteadyChecksquareItemVO(); item.setItemKey(buildItemKey(lineId, indicator, harmonicOrder)); + item.setLineId(lineId); + item.setLineName(lineName); item.setIndicatorCode(indicator.getIndicatorCode()); item.setIndicatorName(indicator.getName()); item.setHarmonicOrder(harmonicOrder); @@ -822,7 +1003,7 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { if (param == null) { throw fail("数据校验参数不能为空"); } - if (trimToNull(param.getLineId()) == null) { + if (resolveLineIds(param).isEmpty()) { throw fail("监测点ID不能为空"); } parseRequiredTime(param.getTimeStart(), "开始时间不能为空"); @@ -833,12 +1014,9 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { if (param == null) { throw fail("数据校验参数不能为空"); } - if (trimToNull(param.getLineId()) == null) { + if (resolveLineIds(param).isEmpty()) { throw fail("监测点ID不能为空"); } - if (!allowEmptyIndicators && normalizeTextList(param.getIndicatorCodes()).isEmpty()) { - throw fail("指标不能为空"); - } parseRequiredTime(param.getTimeStart(), "开始时间不能为空"); parseRequiredTime(param.getTimeEnd(), "结束时间不能为空"); } @@ -872,6 +1050,26 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { return linePath; } + private Map requireLinePaths(List lineIds) { + Map linePathMap = addLedgerService.listLinePathByLineIds(lineIds); + for (String lineId : lineIds) { + if (linePathMap == null || linePathMap.get(lineId) == null) { + throw fail("鐩戞祴鐐逛笉瀛樺湪鎴栦笉鍙敤"); + } + } + return linePathMap; + } + + private String buildLineNames(List lineIds, Map linePathMap) { + List names = new ArrayList(); + for (String lineId : lineIds) { + AddLedgerLinePathVO linePath = linePathMap.get(lineId); + String lineName = linePath == null ? null : trimToNull(linePath.getLineName()); + names.add(lineName == null ? EMPTY_TEXT : lineName); + } + return String.join("、", names); + } + private int resolveIntervalMinutes(AddLedgerLinePathVO linePath) { Integer interval = linePath.getLineInterval(); if (interval == null || interval <= 0) { @@ -1005,6 +1203,7 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { vo.setTaskId(task.getId()); vo.setTaskNo(task.getTaskNo()); vo.setLineId(task.getLineId()); + vo.setLineIds(readTaskLineIds(task)); vo.setLineName(task.getLineName()); vo.setTimeStart(formatTime(task.getTimeStart())); vo.setTimeEnd(formatTime(task.getTimeEnd())); @@ -1021,6 +1220,8 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { SteadyChecksquareItemVO vo = new SteadyChecksquareItemVO(); vo.setItemId(item.getId()); vo.setItemKey(item.getItemKey()); + vo.setLineId(item.getLineId()); + vo.setLineName(item.getLineName()); vo.setIndicatorCode(item.getIndicatorCode()); vo.setIndicatorName(item.getIndicatorName()); vo.setHarmonicOrder(item.getHarmonicOrder()); @@ -1229,6 +1430,53 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { return new ArrayList(result); } + private List resolveLineIds(SteadyChecksquareQueryParam param) { + List lineIds = normalizeTextList(param == null ? null : param.getLineIds()); + if (lineIds.isEmpty() && param != null && trimToNull(param.getLineId()) != null) { + lineIds.add(trimToNull(param.getLineId())); + } + return lineIds; + } + + private List resolveIndicatorCodes(SteadyChecksquareQueryParam param) { + List indicatorCodes = normalizeTextList(param == null ? null : param.getIndicatorCodes()); + if (!indicatorCodes.isEmpty()) { + return indicatorCodes; + } + List allIndicatorCodes = new ArrayList(); + for (SteadyTrendIndicatorDefinitionBO indicator : indicatorCatalog.listIndicators()) { + allIndicatorCodes.add(indicator.getIndicatorCode()); + } + return allIndicatorCodes; + } + + private List resolveResultLineIds(SteadyChecksquareQueryVO result) { + List lineIds = normalizeTextList(result == null ? null : result.getLineIds()); + if (lineIds.isEmpty() && result != null && trimToNull(result.getLineId()) != null) { + lineIds.add(trimToNull(result.getLineId())); + } + return lineIds; + } + + private List readTaskLineIds(SteadyChecksquareTaskPO task) { + List lineIds = readStringList(task.getLineIdsJson()); + if (lineIds.isEmpty() && trimToNull(task.getLineId()) != null) { + lineIds.add(trimToNull(task.getLineId())); + } + return lineIds; + } + + private SteadyChecksquareQueryParam copyCreateParam(SteadyChecksquareQueryParam param, List indicatorCodes) { + SteadyChecksquareQueryParam result = new SteadyChecksquareQueryParam(); + List lineIds = resolveLineIds(param); + result.setLineId(lineIds.isEmpty() ? null : lineIds.get(0)); + result.setLineIds(new ArrayList(lineIds)); + result.setIndicatorCodes(new ArrayList(indicatorCodes)); + result.setTimeStart(param.getTimeStart()); + result.setTimeEnd(param.getTimeEnd()); + return result; + } + private String trimToNull(String value) { if (value == null) { return null; @@ -1246,12 +1494,16 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { } private String buildIndicatorCodesText(List indicatorCodes) { - if (indicatorCodes == null || indicatorCodes.isEmpty()) { + return buildTextListSearchValue(indicatorCodes); + } + + private String buildTextListSearchValue(List values) { + if (values == null || values.isEmpty()) { return null; } StringBuilder builder = new StringBuilder("|"); - for (String indicatorCode : indicatorCodes) { - builder.append(indicatorCode).append("|"); + for (String value : values) { + builder.append(value).append("|"); } return builder.toString(); } @@ -1264,6 +1516,14 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { return text.length() > 2000 ? text.substring(0, 2000) : text; } + private List parseTextListSearchValue(String value) { + String text = trimToNull(value); + if (text == null) { + return new ArrayList(); + } + return normalizeTextList(Arrays.asList(text.split("\\|"))); + } + private List readIntegerList(String json) { if (trimToNull(json) == null) { return new ArrayList(); @@ -1305,17 +1565,17 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { } private static class CreateContext { - private final String lineId; - private final AddLedgerLinePathVO linePath; + private final List lineIds; + private final Map linePathMap; private final LocalDateTime startTime; private final LocalDateTime endTime; private final int intervalMinutes; private final List indicatorCodes; - private CreateContext(String lineId, AddLedgerLinePathVO linePath, LocalDateTime startTime, + private CreateContext(List lineIds, Map linePathMap, LocalDateTime startTime, LocalDateTime endTime, int intervalMinutes, List indicatorCodes) { - this.lineId = lineId; - this.linePath = linePath; + this.lineIds = lineIds; + this.linePathMap = linePathMap; this.startTime = startTime; this.endTime = endTime; this.intervalMinutes = intervalMinutes; diff --git a/steady/check-square/src/main/resources/sql/check-square/steady-checksquare-result-init.sql b/steady/check-square/src/main/resources/sql/check-square/steady-checksquare-result-init.sql index 8e472e8..966b652 100644 --- a/steady/check-square/src/main/resources/sql/check-square/steady-checksquare-result-init.sql +++ b/steady/check-square/src/main/resources/sql/check-square/steady-checksquare-result-init.sql @@ -3,6 +3,8 @@ CREATE TABLE IF NOT EXISTS `steady_checksquare_task` ( `task_no` VARCHAR(64) NOT NULL COMMENT '检测任务编号', `line_id` VARCHAR(64) NOT NULL COMMENT '监测点ID', `line_name` VARCHAR(255) NULL COMMENT '监测点名称', + `line_ids_json` JSON NULL COMMENT '请求监测点ID列表', + `line_ids_text` VARCHAR(2000) NULL COMMENT '请求监测点ID检索文本,格式 |line1|line2|', `time_start` DATETIME NOT NULL COMMENT '检测开始时间', `time_end` DATETIME NOT NULL COMMENT '检测结束时间', `interval_minutes` INT NULL COMMENT '默认统计间隔,单位分钟', @@ -22,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `steady_checksquare_task` ( UNIQUE KEY `uk_steady_checksquare_task_no` (`task_no`), KEY `idx_steady_checksquare_task_line_time` (`line_id`, `time_start`, `time_end`), KEY `idx_steady_checksquare_task_status` (`task_status`), + KEY `idx_steady_checksquare_task_line_ids_text` (`line_ids_text`(255)), KEY `idx_steady_checksquare_task_indicator_text` (`indicator_codes_text`(255)), KEY `idx_steady_checksquare_task_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='稳态数据校验任务表'; @@ -30,6 +33,8 @@ CREATE TABLE IF NOT EXISTS `steady_checksquare_item` ( `id` VARCHAR(64) NOT NULL COMMENT '主键', `task_id` VARCHAR(64) NOT NULL COMMENT '检测任务ID', `item_key` VARCHAR(255) NOT NULL COMMENT '检测项唯一键', + `line_id` VARCHAR(64) NULL COMMENT '监测点ID', + `line_name` VARCHAR(255) NULL COMMENT '监测点名称', `indicator_code` VARCHAR(64) NOT NULL COMMENT '指标编码', `indicator_name` VARCHAR(255) NULL COMMENT '指标名称', `harmonic_order` INT NULL COMMENT '谐波次数;聚合项为空', diff --git a/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java b/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java index d2d43f0..c6baecc 100644 --- a/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java +++ b/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareInfluxQueryComponentTest.java @@ -142,6 +142,71 @@ class SteadyChecksquareInfluxQueryComponentTest { } } + @Test + void shouldQueryMultipleStatTypesOnce() throws Exception { + AtomicInteger requestCount = new AtomicInteger(); + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/query", exchange -> { + requestCount.incrementAndGet(); + byte[] body = ("{\"results\":[{\"series\":[" + + "{\"tags\":{\"value_type\":\"MAX\"},\"values\":[[\"2026-05-01T00:00:00Z\",10]]}," + + "{\"tags\":{\"value_type\":\"AVG\"},\"values\":[[\"2026-05-01T00:00:00Z\",8]]}" + + "]}]}").getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + server.start(); + try { + SteadyInfluxDbProperties properties = new SteadyInfluxDbProperties(); + properties.setUrl("http://127.0.0.1:" + server.getAddress().getPort()); + properties.setDatabase("steady"); + SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(properties); + SteadyTrendResolvedFieldBO max = buildField("rms"); + max.setMeasurement("data_v"); + max.setStatType("MAX"); + SteadyTrendResolvedFieldBO avg = buildField("rms"); + avg.setMeasurement("data_v"); + avg.setStatType("AVG"); + + component.enableRequestCache(); + Map> result = + component.queryStatValuePointMap(Arrays.asList(max, avg), + LocalDateTime.of(2026, 5, 1, 0, 0, 0), + LocalDateTime.of(2026, 5, 1, 0, 1, 0), 1); + component.clearRequestCache(); + + Assertions.assertEquals(1, requestCount.get()); + Assertions.assertEquals(1, result.get("MAX").size()); + Assertions.assertEquals(1, result.get("AVG").size()); + Assertions.assertEquals(new java.math.BigDecimal("10"), result.get("MAX").get(0).getValue()); + Assertions.assertEquals(new java.math.BigDecimal("8"), result.get("AVG").get(0).getValue()); + } finally { + server.stop(0); + } + } + + @Test + void shouldBuildStatValuePointQueryWithValueTypeRegex() { + SteadyChecksquareInfluxQueryComponent component = new SteadyChecksquareInfluxQueryComponent(new SteadyInfluxDbProperties()); + SteadyTrendResolvedFieldBO max = buildField("rms"); + max.setMeasurement("data_v"); + max.setStatType("MAX"); + SteadyTrendResolvedFieldBO avg = buildField("rms"); + avg.setMeasurement("data_v"); + avg.setStatType("AVG"); + + String query = component.buildStatValuePointQuery(Arrays.asList(max, avg), + LocalDateTime.of(2026, 5, 1, 0, 0, 0), + LocalDateTime.of(2026, 5, 1, 0, 1, 0)); + + Assertions.assertTrue(query.contains("SELECT \"rms\" AS \"value\"")); + Assertions.assertTrue(query.contains("\"value_type\" =~ /^(MAX|AVG)$/")); + Assertions.assertTrue(query.contains("GROUP BY \"value_type\"")); + Assertions.assertTrue(query.endsWith("ORDER BY time ASC")); + } + + @Test void shouldSplitLongValuePointQueryByDay() throws Exception { AtomicInteger requestCount = new AtomicInteger(); diff --git a/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java b/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java index 748c4e4..84e1070 100644 --- a/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java +++ b/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/component/SteadyChecksquareValueOrderRuleComponentTest.java @@ -11,6 +11,9 @@ import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -28,23 +31,12 @@ class SteadyChecksquareValueOrderRuleComponentTest { SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); LocalDateTime firstTime = LocalDateTime.of(2026, 5, 1, 0, 0); LocalDateTime secondTime = LocalDateTime.of(2026, 5, 1, 0, 1); - when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) - .thenAnswer(invocation -> { - String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType(); - if ("MAX".equals(statType)) { - return Arrays.asList(point(firstTime, "8"), point(secondTime, "9")); - } - if ("CP95".equals(statType)) { - return Arrays.asList(point(firstTime, "9"), point(secondTime, "10")); - } - if ("AVG".equals(statType)) { - return Arrays.asList(point(firstTime, "7"), point(secondTime, "8")); - } - if ("MIN".equals(statType)) { - return Arrays.asList(point(firstTime, "1"), point(secondTime, "9")); - } - return Collections.emptyList(); - }); + when(influxQueryComponent.queryStatValuePointMap(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenReturn(statPointMap( + Arrays.asList(point(firstTime, "8"), point(secondTime, "9")), + Arrays.asList(point(firstTime, "9"), point(secondTime, "10")), + Arrays.asList(point(firstTime, "7"), point(secondTime, "8")), + Arrays.asList(point(firstTime, "1"), point(secondTime, "9")))); SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null, LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 2), 1); @@ -65,23 +57,12 @@ class SteadyChecksquareValueOrderRuleComponentTest { SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); - when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) - .thenAnswer(invocation -> { - String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType(); - if ("MAX".equals(statType)) { - return Collections.singletonList(point(time, "10")); - } - if ("CP95".equals(statType)) { - return Collections.singletonList(point(time, "10")); - } - if ("AVG".equals(statType)) { - return Collections.singletonList(point(time, "8")); - } - if ("MIN".equals(statType)) { - return Collections.singletonList(point(time, "8")); - } - return Collections.emptyList(); - }); + when(influxQueryComponent.queryStatValuePointMap(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenReturn(statPointMap( + Collections.singletonList(point(time, "10")), + Collections.singletonList(point(time, "10")), + Collections.singletonList(point(time, "8")), + Collections.singletonList(point(time, "8")))); SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null, LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1); @@ -96,23 +77,12 @@ class SteadyChecksquareValueOrderRuleComponentTest { SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); - when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) - .thenAnswer(invocation -> { - String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType(); - if ("MAX".equals(statType)) { - return Collections.singletonList(point(time, "8")); - } - if ("CP95".equals(statType)) { - return Collections.singletonList(point(time, "10")); - } - if ("AVG".equals(statType)) { - return Collections.singletonList(point(time, "8")); - } - if ("MIN".equals(statType)) { - return Collections.singletonList(point(time, "1")); - } - return Collections.emptyList(); - }); + when(influxQueryComponent.queryStatValuePointMap(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenReturn(statPointMap( + Collections.singletonList(point(time, "8")), + Collections.singletonList(point(time, "10")), + Collections.singletonList(point(time, "8")), + Collections.singletonList(point(time, "1")))); SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null, LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1); @@ -127,23 +97,12 @@ class SteadyChecksquareValueOrderRuleComponentTest { SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); - when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) - .thenAnswer(invocation -> { - String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType(); - if ("MAX".equals(statType)) { - return Collections.singletonList(point(time, "8")); - } - if ("CP95".equals(statType)) { - return Collections.singletonList(point(time, "10")); - } - if ("AVG".equals(statType)) { - return Collections.singletonList(point(time, "8")); - } - if ("MIN".equals(statType)) { - return Collections.singletonList(point(time, "1")); - } - return Collections.emptyList(); - }); + when(influxQueryComponent.queryStatValuePointMap(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenReturn(statPointMap( + Collections.singletonList(point(time, "8")), + Collections.singletonList(point(time, "10")), + Collections.singletonList(point(time, "8")), + Collections.singletonList(point(time, "1")))); SteadyTrendIndicatorDefinitionBO indicator = indicator(); indicator.setHarmonic(true); indicator.setHarmonicFieldPrefix("v"); @@ -160,20 +119,12 @@ class SteadyChecksquareValueOrderRuleComponentTest { SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); SteadyChecksquareValueOrderRuleComponent component = new SteadyChecksquareValueOrderRuleComponent(influxQueryComponent); LocalDateTime time = LocalDateTime.of(2026, 5, 1, 0, 0); - when(influxQueryComponent.queryValuePoints(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) - .thenAnswer(invocation -> { - String statType = invocation.getArgument(0, com.njcn.gather.steady.datavie.pojo.bo.SteadyTrendResolvedFieldBO.class).getStatType(); - if ("MAX".equals(statType)) { - return Collections.singletonList(point(time, "10")); - } - if ("CP95".equals(statType)) { - return Collections.singletonList(point(time, "11")); - } - if ("MIN".equals(statType)) { - return Collections.singletonList(point(time, "1")); - } - return Collections.emptyList(); - }); + when(influxQueryComponent.queryStatValuePointMap(any(), any(LocalDateTime.class), any(LocalDateTime.class), eq(1))) + .thenReturn(statPointMap( + Collections.singletonList(point(time, "10")), + Collections.singletonList(point(time, "11")), + Collections.emptyList(), + Collections.singletonList(point(time, "1")))); SteadyChecksquareValueOrderRuleVO result = component.check("line-001", indicator(), null, LocalDateTime.of(2026, 5, 1, 0, 0), LocalDateTime.of(2026, 5, 1, 0, 1), 1); @@ -216,4 +167,17 @@ class SteadyChecksquareValueOrderRuleComponentTest { point.setValue(new BigDecimal(value)); return point; } + + private Map> statPointMap(List maxPoints, + List cp95Points, + List avgPoints, + List minPoints) { + Map> result = + new LinkedHashMap>(); + result.put("MAX", maxPoints); + result.put("CP95", cp95Points); + result.put("AVG", avgPoints); + result.put("MIN", minPoints); + return result; + } } diff --git a/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareControllerTest.java b/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareControllerTest.java index 52c0aac..26b7041 100644 --- a/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareControllerTest.java +++ b/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/controller/SteadyChecksquareControllerTest.java @@ -28,6 +28,10 @@ class SteadyChecksquareControllerTest { PostMapping createMapping = createMethod.getAnnotation(PostMapping.class); Assertions.assertArrayEquals(new String[]{"/create"}, createMapping.value()); + Method restartMethod = SteadyChecksquareController.class.getDeclaredMethod("restart", String.class); + PostMapping restartMapping = restartMethod.getAnnotation(PostMapping.class); + Assertions.assertArrayEquals(new String[]{"/restart"}, restartMapping.value()); + Method detailMethod = SteadyChecksquareController.class.getDeclaredMethod("detail", String.class); GetMapping detailMapping = detailMethod.getAnnotation(GetMapping.class); Assertions.assertArrayEquals(new String[]{"/detail"}, detailMapping.value()); diff --git a/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java b/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java index 44a349c..bf5990b 100644 --- a/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java +++ b/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java @@ -48,6 +48,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import static org.mockito.ArgumentMatchers.any; @@ -68,6 +69,7 @@ class SteadyChecksquareServiceImplTest { MapperBuilderAssistant assistant = new MapperBuilderAssistant(new Configuration(), ""); TableInfoHelper.initTableInfo(assistant, SteadyChecksquareTaskPO.class); TableInfoHelper.initTableInfo(assistant, SteadyChecksquareItemPO.class); + TableInfoHelper.initTableInfo(assistant, SteadyChecksquareStatSummaryPO.class); TableInfoHelper.initTableInfo(assistant, SteadyChecksquareDetailPO.class); } @@ -153,7 +155,8 @@ class SteadyChecksquareServiceImplTest { SteadyChecksquareTaskVO result = service.create(param); Assertions.assertEquals(taskCaptor.getValue().getId(), result.getTaskId()); - Assertions.assertEquals(Integer.valueOf(5), result.getItemCount()); + Assertions.assertEquals("RUNNING", result.getTaskStatus()); + Assertions.assertEquals(Integer.valueOf(0), result.getItemCount()); verify(itemService).saveBatch(any()); } @@ -201,7 +204,7 @@ class SteadyChecksquareServiceImplTest { } @Test - void shouldCreateTaskSynchronouslyAndReturnTaskSummary() { + void shouldCreateRunningTaskAndReturnTaskSummaryBeforeCalculationCompletes() { AddLedgerService addLedgerService = mock(AddLedgerService.class); SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class); SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); @@ -243,12 +246,69 @@ class SteadyChecksquareServiceImplTest { SteadyChecksquareTaskVO result = service.create(param); Assertions.assertEquals(taskCaptor.getValue().getId(), result.getTaskId()); - Assertions.assertEquals("SUCCESS", result.getTaskStatus()); - Assertions.assertEquals(Integer.valueOf(1), result.getItemCount()); + Assertions.assertEquals("RUNNING", result.getTaskStatus()); + Assertions.assertEquals(Integer.valueOf(0), result.getItemCount()); verify(itemService).saveBatch(any()); verify(statSummaryService).saveBatch(any()); } + @Test + void shouldCreateSingleRunningTaskForMultipleLines() { + AddLedgerService addLedgerService = mock(AddLedgerService.class); + SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class); + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class); + SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class); + SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class); + SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class); + SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class); + LambdaQueryChainWrapper taskQuery = mock(LambdaQueryChainWrapper.class); + when(taskService.lambdaQuery()).thenReturn(taskQuery); + when(taskQuery.eq(any(), any())).thenReturn(taskQuery); + when(taskQuery.and(any())).thenReturn(taskQuery); + when(taskQuery.orderByDesc(any(SFunction.class))).thenReturn(taskQuery); + when(taskQuery.list()).thenReturn(Collections.emptyList()); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + influxQueryComponent, new SteadyChecksquareCalculator(), + valueOrderRuleComponent, harmonicParityRuleComponent, + new AddDataTimeSlotCalculator(), addLedgerService, taskService, + itemService, statSummaryService, detailService, new ObjectMapper()); + AddLedgerLinePathVO linePath1 = new AddLedgerLinePathVO(); + linePath1.setLineId("line-001"); + linePath1.setLineName("line-001"); + linePath1.setLineInterval(1); + AddLedgerLinePathVO linePath2 = new AddLedgerLinePathVO(); + linePath2.setLineId("line-002"); + linePath2.setLineName("line-002"); + linePath2.setLineInterval(1); + LinkedHashMap linePathMap = new LinkedHashMap(); + linePathMap.put("line-001", linePath1); + linePathMap.put("line-002", linePath2); + when(addLedgerService.listLinePathByLineIds(eq(Arrays.asList("line-001", "line-002")))) + .thenReturn(linePathMap); + when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(new HashSet()); + when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyRuleResult()); + when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyHarmonicParityRuleResult()); + ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(SteadyChecksquareTaskPO.class); + when(taskService.save(taskCaptor.capture())).thenReturn(true); + SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam(); + param.setLineIds(Arrays.asList("line-001", "line-002")); + param.setIndicatorCodes(Collections.singletonList("V_RMS")); + param.setTimeStart("2026-05-01 00:00:00"); + param.setTimeEnd("2026-05-01 00:01:00"); + + SteadyChecksquareTaskVO result = service.create(param); + + Assertions.assertEquals(taskCaptor.getValue().getId(), result.getTaskId()); + Assertions.assertEquals(Arrays.asList("line-001", "line-002"), result.getLineIds()); + Assertions.assertEquals("|line-001|line-002|", taskCaptor.getValue().getLineIdsText()); + Assertions.assertEquals("line-001、line-002", taskCaptor.getValue().getLineName()); + verify(taskService, times(1)).save(any()); + } + @Test void shouldUseFixedFlickerIntervalsPerIndicator() { SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); @@ -293,6 +353,50 @@ class SteadyChecksquareServiceImplTest { assertItemInterval(result.getItems().get(2), "PLT", 120, 2); } + @Test + void shouldUseAllIndicatorsWhenCreateIndicatorCodesIsEmpty() { + SteadyTrendIndicatorCatalog indicatorCatalog = new SteadyTrendIndicatorCatalog(); + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class); + SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class); + AddLedgerService addLedgerService = mock(AddLedgerService.class); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(indicatorCatalog, + influxQueryComponent, new SteadyChecksquareCalculator(), valueOrderRuleComponent, harmonicParityRuleComponent, + new AddDataTimeSlotCalculator(), addLedgerService, mock(SteadyChecksquareTaskService.class), + mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class), + mock(SteadyChecksquareDetailService.class), new ObjectMapper()); + when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyRuleResult()); + when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyHarmonicParityRuleResult()); + AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); + linePath.setLineId("line-001"); + linePath.setLineName("line-001"); + linePath.setLineInterval(1); + when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) + .thenReturn(Collections.singletonMap("line-001", linePath)); + when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(new HashSet()); + + SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam(); + param.setLineId("line-001"); + param.setIndicatorCodes(Collections.emptyList()); + param.setTimeStart("2026-05-01 00:00:00"); + param.setTimeEnd("2026-05-01 00:01:00"); + + SteadyChecksquareQueryVO result = calculate(service, param); + + List expectedIndicatorCodes = new ArrayList(); + for (SteadyTrendIndicatorDefinitionBO indicator : indicatorCatalog.listIndicators()) { + expectedIndicatorCodes.add(indicator.getIndicatorCode()); + } + List actualIndicatorCodes = new ArrayList(); + for (SteadyChecksquareItemVO item : result.getItems()) { + actualIndicatorCodes.add(item.getIndicatorCode()); + } + Assertions.assertEquals(expectedIndicatorCodes, actualIndicatorCodes); + } + @Test void shouldAggregateAllHarmonicOrdersIntoIndicatorItem() { SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); @@ -566,7 +670,11 @@ class SteadyChecksquareServiceImplTest { task.setTimeEnd(LocalDateTime.of(2026, 5, 1, 0, 1)); task.setIntervalMinutes(1); SteadyChecksquareItemPO item1 = buildItemPO("item-001", "V_RMS"); + item1.setLineId("line-001"); + item1.setLineName("line-001-name"); SteadyChecksquareItemPO item2 = buildItemPO("item-002", "FREQ"); + item2.setLineId("line-002"); + item2.setLineName("line-002-name"); SteadyChecksquareStatSummaryPO summary1 = buildSummaryPO("item-001", "AVG"); SteadyChecksquareStatSummaryPO summary2 = buildSummaryPO("item-002", "AVG"); when(taskService.getById("task-001")).thenReturn(task); @@ -585,6 +693,10 @@ class SteadyChecksquareServiceImplTest { SteadyChecksquareQueryVO result = service.detail("task-001"); Assertions.assertEquals(2, result.getItems().size()); + Assertions.assertEquals("line-001", result.getItems().get(0).getLineId()); + Assertions.assertEquals("line-001-name", result.getItems().get(0).getLineName()); + Assertions.assertEquals("line-002", result.getItems().get(1).getLineId()); + Assertions.assertEquals("line-002-name", result.getItems().get(1).getLineName()); Assertions.assertEquals(1, result.getItems().get(0).getStatSummaries().size()); Assertions.assertEquals(1, result.getItems().get(1).getStatSummaries().size()); verify(statSummaryService, times(1)).lambdaQuery(); @@ -596,6 +708,8 @@ class SteadyChecksquareServiceImplTest { SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class); SteadyChecksquareItemPO item = new SteadyChecksquareItemPO(); item.setId("item-001"); + item.setLineId("line-001"); + item.setLineName("line-001-name"); item.setState(1); SteadyChecksquareDetailPO detail = new SteadyChecksquareDetailPO(); detail.setItemId("item-001"); @@ -619,6 +733,8 @@ class SteadyChecksquareServiceImplTest { Assertions.assertEquals(Integer.valueOf(2), result.getPageNum()); Assertions.assertEquals(Integer.valueOf(1), result.getPageSize()); Assertions.assertEquals(Long.valueOf(1L), result.getTotal()); + Assertions.assertEquals("line-001", result.getLineId()); + Assertions.assertEquals("line-001-name", result.getLineName()); Assertions.assertEquals(1, result.getValueOrderDetails().size()); verify(detailService).page(any(Page.class), any()); } @@ -651,6 +767,75 @@ class SteadyChecksquareServiceImplTest { verify(itemService).update(any()); } + @Test + void shouldRejectRestartWhenTaskIsNotFail() { + SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class); + SteadyChecksquareTaskPO task = buildTask("task-001", "SUCCESS"); + when(taskService.getById("task-001")).thenReturn(task); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(), + mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class), + new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService, + mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class), + mock(SteadyChecksquareDetailService.class), new ObjectMapper()); + + Assertions.assertThrows(RuntimeException.class, () -> service.restart("task-001")); + + verify(taskService, never()).update(any()); + } + + @Test + void shouldRestartFailTaskWithSameTaskIdAndOriginalParam() { + AddLedgerService addLedgerService = mock(AddLedgerService.class); + SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class); + SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class); + SteadyChecksquareValueOrderRuleComponent valueOrderRuleComponent = mock(SteadyChecksquareValueOrderRuleComponent.class); + SteadyChecksquareHarmonicParityRuleComponent harmonicParityRuleComponent = mock(SteadyChecksquareHarmonicParityRuleComponent.class); + SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class); + SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class); + SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class); + LambdaQueryChainWrapper itemQuery = mock(LambdaQueryChainWrapper.class); + SteadyChecksquareTaskPO task = buildTask("task-001", "FAIL"); + SteadyChecksquareItemPO oldItem = new SteadyChecksquareItemPO(); + oldItem.setId("item-001"); + oldItem.setTaskId("task-001"); + when(taskService.getById("task-001")).thenReturn(task); + when(taskService.update(any())).thenReturn(true); + when(itemService.lambdaQuery()).thenReturn(itemQuery); + when(itemQuery.eq(any(), any())).thenReturn(itemQuery); + when(itemQuery.list()).thenReturn(Collections.singletonList(oldItem)); + when(itemService.remove(any())).thenReturn(true); + when(statSummaryService.remove(any())).thenReturn(true); + when(detailService.remove(any())).thenReturn(true); + AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); + linePath.setLineId("line-001"); + linePath.setLineName("line-001"); + linePath.setLineInterval(1); + when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) + .thenReturn(Collections.singletonMap("line-001", linePath)); + when(influxQueryComponent.queryExistingSlots(any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(new HashSet()); + when(valueOrderRuleComponent.check(any(), any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyRuleResult()); + when(harmonicParityRuleComponent.check(any(), any(), any(LocalDateTime.class), any(LocalDateTime.class), anyInt())) + .thenReturn(emptyHarmonicParityRuleResult()); + SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(), + influxQueryComponent, new SteadyChecksquareCalculator(), + valueOrderRuleComponent, harmonicParityRuleComponent, + new AddDataTimeSlotCalculator(), addLedgerService, taskService, + itemService, statSummaryService, detailService, new ObjectMapper()); + + SteadyChecksquareTaskVO result = service.restart("task-001"); + + Assertions.assertEquals("task-001", result.getTaskId()); + Assertions.assertEquals("RUNNING", result.getTaskStatus()); + verify(detailService).remove(any()); + verify(statSummaryService).remove(any()); + verify(itemService).remove(any()); + verify(taskService, times(2)).update(any()); + verify(itemService).saveBatch(any()); + } + @Test void shouldSaveChecksquareResultsInBatch() { SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class); @@ -672,6 +857,8 @@ class SteadyChecksquareServiceImplTest { result.setIntervalMinutes(1); SteadyChecksquareItemVO item = new SteadyChecksquareItemVO(); item.setItemKey("line-001|V_RMS"); + item.setLineId("line-001"); + item.setLineName("line-001-name"); item.setIndicatorCode("V_RMS"); item.setIndicatorName("鐩哥數鍘嬫湁鏁堝€?"); item.setIntervalMinutes(1); @@ -700,7 +887,11 @@ class SteadyChecksquareServiceImplTest { saveResult(service, param, result); verify(taskService).save(any()); - verify(itemService).saveBatch(any()); + ArgumentCaptor itemCaptor = ArgumentCaptor.forClass(List.class); + verify(itemService).saveBatch(itemCaptor.capture()); + SteadyChecksquareItemPO savedItem = (SteadyChecksquareItemPO) itemCaptor.getValue().get(0); + Assertions.assertEquals("line-001", savedItem.getLineId()); + Assertions.assertEquals("line-001-name", savedItem.getLineName()); verify(statSummaryService).saveBatch(any()); } @@ -846,9 +1037,9 @@ class SteadyChecksquareServiceImplTest { List orderItems) { try { Method method = SteadyChecksquareServiceImpl.class.getDeclaredMethod("aggregateHarmonicItems", - String.class, SteadyTrendIndicatorDefinitionBO.class, List.class, int.class); + String.class, String.class, SteadyTrendIndicatorDefinitionBO.class, List.class, int.class); method.setAccessible(true); - return (SteadyChecksquareItemVO) method.invoke(service, "line-001", indicator, orderItems, 1); + return (SteadyChecksquareItemVO) method.invoke(service, "line-001", "line-001-name", indicator, orderItems, 1); } catch (Exception exception) { throw new RuntimeException(exception); } @@ -862,6 +1053,29 @@ class SteadyChecksquareServiceImplTest { return indicator; } + private SteadyChecksquareTaskPO buildTask(String taskId, String taskStatus) { + SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO(); + task.setId(taskId); + task.setTaskNo("CS202605010001"); + task.setLineId("line-001"); + task.setLineName("line-001"); + task.setLineIdsJson("[\"line-001\"]"); + task.setLineIdsText("|line-001|"); + task.setTimeStart(LocalDateTime.of(2026, 5, 1, 0, 0)); + task.setTimeEnd(LocalDateTime.of(2026, 5, 1, 0, 1)); + task.setIntervalMinutes(1); + task.setIndicatorCodesJson("[\"V_RMS\"]"); + task.setIndicatorCodesText("|V_RMS|"); + task.setTaskStatus(taskStatus); + task.setItemCount(1); + task.setAbnormalItemCount(1); + task.setMinDataIntegrity(BigDecimal.ZERO.setScale(6)); + task.setResultMessage("failed"); + task.setState(1); + task.setCreateTime(LocalDateTime.of(2026, 5, 1, 1, 0)); + return task; + } + private SteadyChecksquareItemVO buildOrderItem(boolean hasData, BigDecimal dataIntegrity) { SteadyChecksquareItemVO item = new SteadyChecksquareItemVO(); item.setItemKey("line-001|V_HARMONIC|2"); diff --git a/tools/device-types/src/main/java/com/njcn/gather/device/types/mapper/mapping/CsDevTypeMapper.xml b/tools/device-types/src/main/java/com/njcn/gather/device/types/mapper/mapping/CsDevTypeMapper.xml index e62e029..d3da472 100644 --- a/tools/device-types/src/main/java/com/njcn/gather/device/types/mapper/mapping/CsDevTypeMapper.xml +++ b/tools/device-types/src/main/java/com/njcn/gather/device/types/mapper/mapping/CsDevTypeMapper.xml @@ -30,7 +30,7 @@ d.name AS name, d.icd AS icdId, p.Name AS icdName, - p.Path AS icdPath, + NULL AS icdPath, p.Result AS icdResult, p.Msg AS icdMsg, d.power AS power, diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IcdConsistencyCheckService.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IcdConsistencyCheckService.java index 5c25a81..fc97300 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IcdConsistencyCheckService.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IcdConsistencyCheckService.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -37,6 +38,9 @@ public class IcdConsistencyCheckService { private static final String ISSUE_FILE_NAME = "icd-consistency-issues.json"; private static final List REQUIRED_REPORT_DESCS = Arrays.asList("统计数据", "波动闪变", "实时数据", "暂态事件"); private static final List REQUIRED_LN_CLASSES = Arrays.asList("MMXU", "MSQI", "MHAI", "MFLK"); + private static final Set EXCLUDED_DOI_DESCS = new HashSet(Arrays.asList( + "电压扰动事件启动", "电压暂降事件启动", "电压暂升事件启动", "电压中断事件启动", + "电压暂降启动定值", "电压暂升启动定值", "电压中断启动定值")); private final FileStorageService fileStorageService; private final ObjectMapper objectMapper = buildMapper(); @@ -45,14 +49,13 @@ public class IcdConsistencyCheckService { if (request == null) { throw new IllegalArgumentException("ICD 一致性校验请求不能为空"); } - log.info("ICD一致性校验,标准ICD JSON={}", request.getStandardJson()); - log.info("ICD一致性校验,待校验ICD JSON={}", request.getCheckedJson()); MappingDocument checked = parseMapping(request.getCheckedJson(), "待校验 JSON"); MappingDocument standard = parseMapping(request.getStandardJson(), "标准 JSON"); List issues = new ArrayList(); validateSelfFormat(checked, "待校验映射", issues); - boolean corrected = applySelfMappingRules(checked, issues); + boolean corrected = applySelfMappingRules(checked, standard); + corrected = removeDuplicateDescDoiItems(checked, standard, issues) || corrected; validateConsistency(checked, standard, issues); IcdConsistencyCheckResponse response = new IcdConsistencyCheckResponse(); @@ -172,6 +175,9 @@ public class IcdConsistencyCheckService { continue; } for (DoiItem doi : inst.getDoiList()) { + if (isExcludedDoiDesc(doi.getDesc())) { + continue; + } if (isEmpty(doi.getSdiList())) { addIssue(issues, "自身格式校验", instPath + ".doiList[" + buildKey(doi.getName(), doi.getDesc()) + "]", "typeList 不能为空:" + joinDesc(group.getDesc(), inst.getDesc(), doi.getDesc()), null, null, false); @@ -197,24 +203,28 @@ public class IcdConsistencyCheckService { validateDataSetConsistency(checked, standard, issues); } - private boolean applySelfMappingRules(MappingDocument checked, List issues) { + private boolean applySelfMappingRules(MappingDocument checked, MappingDocument standard) { if (checked.getReportMap() == null) { return false; } + Map standardMap = indexReportMap(standard); boolean hasRtFre = false; boolean corrected = false; for (ReportMapItem item : checked.getReportMap()) { - if ("实时数据".equals(trimToEmpty(item.getDesc())) && trimToEmpty(item.getRptId()).contains("RtFre")) { + if ("实时数据".equals(trimToEmpty(item.getDesc())) && containsRtFre(item.getRptId())) { hasRtFre = true; + ReportMapItem standardItem = standardMap.get(buildReportKey(item)); + boolean needRuleCorrection = standardItem == null + || !equalsValue(String.valueOf(standardItem.getReportCount()), String.valueOf(item.getReportCount())) + || !equalsValue(standardItem.getFlickerFlag(), item.getFlickerFlag()); + if (!needRuleCorrection) { + continue; + } if (!"1".equals(trimToEmpty(item.getFlickerFlag()))) { - addIssue(issues, "映射规则", "ReportMap[" + buildReportKey(item) + "].FlickerFlag", - "实时数据报告 rptID 包含 RtFre,FlickerFlag 已按规则调整为 1", "1", item.getFlickerFlag(), true); item.setFlickerFlag("1"); corrected = true; } if (item.getReportCount() != 0) { - addIssue(issues, "映射规则", "ReportMap[" + buildReportKey(item) + "].reportCount", - "实时数据报告 rptID 包含 RtFre,reportCount 已按规则调整为 0", "0", String.valueOf(item.getReportCount()), true); item.setReportCount(0); corrected = true; } @@ -225,11 +235,12 @@ public class IcdConsistencyCheckService { } for (ReportMapItem item : checked.getReportMap()) { if ("统计数据".equals(trimToEmpty(item.getDesc()))) { + ReportMapItem standardItem = standardMap.get(buildReportKey(item)); + if (standardItem != null && equalsValue(String.valueOf(standardItem.getReportCount()), String.valueOf(item.getReportCount()))) { + continue; + } int adjustedCount = item.getReportCount() - 1; if (item.getReportCount() != adjustedCount) { - addIssue(issues, "映射规则", "ReportMap[" + buildReportKey(item) + "].reportCount", - "存在 RtFre 实时数据报告,统计数据 reportCount 已按规则减 1", - String.valueOf(adjustedCount), String.valueOf(item.getReportCount()), true); item.setReportCount(adjustedCount); corrected = true; } @@ -238,6 +249,47 @@ public class IcdConsistencyCheckService { return corrected; } + private boolean removeDuplicateDescDoiItems(MappingDocument checked, MappingDocument standard, List issues) { + if (checked.getDataSetList() == null) { + return false; + } + Map> standardDoiKeys = indexStandardDoiKeysByInst(standard); + boolean corrected = false; + for (DataSetGroupItem group : checked.getDataSetList()) { + if (group.getInstList() == null) { + continue; + } + for (InstItem inst : group.getInstList()) { + if (inst.getDoiList() == null) { + continue; + } + Map> doiItemsByDesc = new HashMap>(); + for (DoiItem doi : inst.getDoiList()) { + String desc = trimToEmpty(doi.getDesc()); + if (!doiItemsByDesc.containsKey(desc)) { + doiItemsByDesc.put(desc, new ArrayList()); + } + doiItemsByDesc.get(desc).add(doi); + } + String instPath = buildInstPath(group, inst); + Set currentStandardKeys = standardDoiKeys.get(buildInstKey(group, inst)); + for (Map.Entry> entry : doiItemsByDesc.entrySet()) { + if (entry.getValue().size() <= 1) { + continue; + } + DoiItem retained = chooseRetainedDoi(entry.getValue(), currentStandardKeys); + String message = "同一个 doiList 下存在 desc 相同的指标,已仅保留 " + buildKey(retained.getDesc(), retained.getName()) + + ";重复组合:" + describeDoiStandardMatches(entry.getValue(), currentStandardKeys); + addIssue(issues, "映射规则", instPath + ".doiList[desc=" + entry.getKey() + "]", message, + null, describeDoiKeys(entry.getValue()), true); + removeDuplicatedDoiItems(inst.getDoiList(), entry.getValue(), retained); + corrected = true; + } + } + } + return corrected; + } + private void validateReportMapConsistency(MappingDocument checked, MappingDocument standard, List issues) { Map checkedMap = new HashMap(); if (checked.getReportMap() != null) { @@ -303,6 +355,9 @@ public class IcdConsistencyCheckService { return; } for (DoiItem standardDoi : standardInst.getDoiList()) { + if (isExcludedDoiDesc(standardDoi.getDesc())) { + continue; + } String doiKey = buildKey(standardDoi.getName(), standardDoi.getDesc()); DoiItem checkedDoi = checkedDoiMap.get(doiKey); String path = "DataSetList[" + groupKey + "].instList[" + instKey + "].doiList[" + doiKey + "]"; @@ -343,6 +398,17 @@ public class IcdConsistencyCheckService { issues.add(issue); } + private Map indexReportMap(MappingDocument document) { + Map result = new HashMap(); + if (document.getReportMap() == null) { + return result; + } + for (ReportMapItem item : document.getReportMap()) { + result.put(buildReportKey(item), item); + } + return result; + } + private Map indexGroups(MappingDocument document) { Map result = new HashMap(); if (document.getDataSetList() == null) { @@ -376,10 +442,36 @@ public class IcdConsistencyCheckService { return result; } + private Map> indexStandardDoiKeysByInst(MappingDocument standard) { + Map> result = new HashMap>(); + if (standard.getDataSetList() == null) { + return result; + } + for (DataSetGroupItem group : standard.getDataSetList()) { + if (group.getInstList() == null) { + continue; + } + for (InstItem inst : group.getInstList()) { + Set doiKeys = new HashSet(); + if (inst.getDoiList() != null) { + for (DoiItem doi : inst.getDoiList()) { + doiKeys.add(buildKey(doi.getDesc(), doi.getName())); + } + } + result.put(buildInstKey(group, inst), doiKeys); + } + } + return result; + } + private String buildReportKey(ReportMapItem item) { return buildKey(item.getDesc(), item.getRptId(), item.getName()); } + private String buildInstKey(DataSetGroupItem group, InstItem inst) { + return buildKey(group.getLnClass(), group.getDesc(), inst.getInst(), inst.getDesc()); + } + private String buildGroupPath(DataSetGroupItem group) { return "DataSetList[" + buildKey(group.getLnClass(), group.getDesc()) + "]"; } @@ -396,10 +488,57 @@ public class IcdConsistencyCheckService { return String.join("+", parts); } + private DoiItem chooseRetainedDoi(List doiItems, Set standardDoiKeys) { + if (standardDoiKeys != null) { + for (DoiItem item : doiItems) { + if (standardDoiKeys.contains(buildKey(item.getDesc(), item.getName()))) { + return item; + } + } + } + return doiItems.get(0); + } + + private void removeDuplicatedDoiItems(List allDoiItems, List duplicatedItems, DoiItem retained) { + Iterator iterator = allDoiItems.iterator(); + while (iterator.hasNext()) { + DoiItem item = iterator.next(); + if (duplicatedItems.contains(item) && item != retained) { + iterator.remove(); + } + } + } + + private String describeDoiStandardMatches(List doiItems, Set standardDoiKeys) { + List values = new ArrayList(); + for (DoiItem item : doiItems) { + String key = buildKey(item.getDesc(), item.getName()); + boolean existsInStandard = standardDoiKeys != null && standardDoiKeys.contains(key); + values.add(key + (existsInStandard ? " 在标准映射中存在" : " 不在标准映射中")); + } + return String.join(",", values); + } + + private String describeDoiKeys(List doiItems) { + List values = new ArrayList(); + for (DoiItem item : doiItems) { + values.add(buildKey(item.getDesc(), item.getName())); + } + return String.join(",", values); + } + private boolean equalsValue(String left, String right) { return trimToEmpty(left).equals(trimToEmpty(right)); } + private boolean containsRtFre(String value) { + return trimToEmpty(value).toLowerCase().contains("rtfre"); + } + + private boolean isExcludedDoiDesc(String desc) { + return EXCLUDED_DOI_DESCS.contains(trimToEmpty(desc)); + } + private boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/CsIcdPathController.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/CsIcdPathController.java index 4b907ed..1078a5d 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/CsIcdPathController.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/CsIcdPathController.java @@ -5,8 +5,10 @@ import com.njcn.common.pojo.enums.common.LogEnum; import com.njcn.common.pojo.enums.response.CommonResponseEnum; import com.njcn.common.pojo.response.HttpResult; import com.njcn.common.utils.LogUtil; +import com.fasterxml.jackson.databind.JsonNode; import com.njcn.gather.icd.mapping.pojo.param.CsIcdPathParam; import com.njcn.gather.icd.mapping.pojo.param.IcdCheckResultSaveParam; +import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO; import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO; import com.njcn.gather.icd.mapping.service.CsIcdPathService; import com.njcn.web.controller.BaseController; @@ -50,6 +52,16 @@ public class CsIcdPathController extends BaseController { return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); } + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询参照ICD列表") + @PostMapping("/reference-list") + public HttpResult> referenceList() { + String methodDescribe = getMethodDescribe("referenceList"); + LogUtil.njcnDebug(log, "{},开始查询参照ICD列表", methodDescribe); + List result = csIcdPathService.listReferenceIcdPaths(); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + @OperateInfo(info = LogEnum.BUSINESS_COMMON) @ApiOperation("新增ICD存储记录") @PostMapping(value = "/add", consumes = {"application/json"}) @@ -115,6 +127,28 @@ public class CsIcdPathController extends BaseController { return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); } + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询ICD校验结果详情") + @ApiImplicitParam(name = "id", value = "ICD记录ID", required = true) + @PostMapping("/{id}/icd-check-msg") + public HttpResult getIcdCheckMsg(@PathVariable("id") String id) { + String methodDescribe = getMethodDescribe("getIcdCheckMsg"); + LogUtil.njcnDebug(log, "{},开始查询ICD校验结果详情,icdId={}", methodDescribe, id); + JsonNode result = csIcdPathService.getIcdCheckMsg(id); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询ICD映射文件详情") + @ApiImplicitParam(name = "id", value = "ICD记录ID", required = true) + @PostMapping("/{id}/mapping-detail") + public HttpResult getMappingDetail(@PathVariable("id") String id) { + String methodDescribe = getMethodDescribe("getMappingDetail"); + LogUtil.njcnDebug(log, "{},开始查询ICD映射文件详情,icdId={}", methodDescribe, id); + CsIcdPathDetailVO result = csIcdPathService.getMappingDetail(id); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + @OperateInfo(info = LogEnum.BUSINESS_COMMON) @ApiOperation("保存ICD唯一性校验结果") @ApiImplicitParam(name = "id", value = "ICD记录ID", required = true) @@ -147,9 +181,6 @@ public class CsIcdPathController extends BaseController { } try { param.setIcdContent(icdFile.getBytes()); - if (param.getPath() == null || param.getPath().trim().isEmpty()) { - param.setPath(resolveFileName(icdFile)); - } } catch (IOException ex) { throw new IllegalArgumentException("读取ICD文件失败:" + ex.getMessage(), ex); } diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/mapper/CsIcdPathMapper.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/mapper/CsIcdPathMapper.java index 297f440..4cc0d7f 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/mapper/CsIcdPathMapper.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/mapper/CsIcdPathMapper.java @@ -2,6 +2,7 @@ package com.njcn.gather.icd.mapping.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.njcn.gather.icd.mapping.pojo.po.CsIcdPathPO; +import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO; import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO; import org.apache.ibatis.annotations.Param; @@ -15,4 +16,10 @@ public interface CsIcdPathMapper extends BaseMapper { List selectIcdPathList(@Param("keyword") String keyword, @Param("type") Integer type, @Param("result") Integer result); + + List selectReferenceIcdPathList(); + + CsIcdPathVO selectIcdCheckMsgById(@Param("id") String id); + + CsIcdPathDetailVO selectIcdPathDetailById(@Param("id") String id); } diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/mapper/mapping/CsIcdPathMapper.xml b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/mapper/mapping/CsIcdPathMapper.xml index e7dac19..5c38fd3 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/mapper/mapping/CsIcdPathMapper.xml +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/mapper/mapping/CsIcdPathMapper.xml @@ -7,12 +7,9 @@ type="com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO"> - - - @@ -29,12 +26,9 @@ SELECT ID AS id, Name AS name, - Path AS path, Angle AS angle, Use_Phase_Index AS usePhaseIndex, State AS state, - Json_Str AS jsonStr, - Xml_Str AS xmlStr, Result AS result, Msg AS msg, Type AS type, @@ -46,8 +40,7 @@ FROM cs_icd_path WHERE State = 1 - AND (Name LIKE CONCAT('%', #{keyword}, '%') - OR Path LIKE CONCAT('%', #{keyword}, '%')) + AND Name LIKE CONCAT('%', #{keyword}, '%') AND Type = #{type} @@ -58,4 +51,47 @@ ORDER BY Update_Time DESC, Create_Time DESC + + + + + + diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/CsIcdPathParam.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/CsIcdPathParam.java index 62db083..77e65ad 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/CsIcdPathParam.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/CsIcdPathParam.java @@ -18,9 +18,6 @@ public class CsIcdPathParam { @NotBlank(message = "ICD名称不能为空") private String name; - @ApiModelProperty("ICD存储路径") - private String path; - @ApiModelProperty("ICD文件二进制内容") private byte[] icdContent; @@ -30,7 +27,7 @@ public class CsIcdPathParam { @ApiModelProperty("是否使用相位索引") private Integer usePhaseIndex; - @ApiModelProperty("ICD类型,1-标准ICD") + @ApiModelProperty("ICD类型,1-手动录入的标准ICD,2-手动录入的非标准ICD,3-上游解析传递的标准ICD,4-上游解析传递的非标准ICD") private Integer type; /** @@ -53,10 +50,10 @@ public class CsIcdPathParam { @ApiModel("ICD存储记录列表查询参数") public static class ListParam { - @ApiModelProperty("关键字,匹配ICD名称或路径") + @ApiModelProperty("关键字,匹配ICD名称") private String keyword; - @ApiModelProperty("ICD类型") + @ApiModelProperty("ICD类型,1-手动录入的标准ICD,2-手动录入的非标准ICD,3-上游解析传递的标准ICD,4-上游解析传递的非标准ICD") private Integer type; @ApiModelProperty("ICD校验结果,0-否,1-是") diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/po/CsIcdPathPO.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/po/CsIcdPathPO.java index c8c2a80..9669234 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/po/CsIcdPathPO.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/po/CsIcdPathPO.java @@ -25,9 +25,6 @@ public class CsIcdPathPO implements Serializable { @TableField("Name") private String name; - @TableField("Path") - private String path; - @TableField("Icd") private byte[] icdContent; diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/CsIcdPathDetailVO.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/CsIcdPathDetailVO.java new file mode 100644 index 0000000..c19cd99 --- /dev/null +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/CsIcdPathDetailVO.java @@ -0,0 +1,32 @@ +package com.njcn.gather.icd.mapping.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * ICD 映射文件详情。 + */ +@Data +@ApiModel("ICD映射文件详情") +public class CsIcdPathDetailVO { + + @ApiModelProperty("ICD记录ID") + private String id; + + @ApiModelProperty("ICD名称") + private String name; + + @ApiModelProperty("MMS映射JSON") + private String jsonStr; + + @ApiModelProperty("MMS映射XML") + private String xmlStr; + + @ApiModelProperty("ICD源文件文本") + private String icdText; + + @JsonIgnore + private byte[] icdContent; +} diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/CsIcdPathVO.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/CsIcdPathVO.java index 042234f..ef464d7 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/CsIcdPathVO.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/CsIcdPathVO.java @@ -20,9 +20,6 @@ public class CsIcdPathVO { @ApiModelProperty("ICD名称") private String name; - @ApiModelProperty("ICD存储路径") - private String path; - @ApiModelProperty("角度") private Integer angle; @@ -32,19 +29,13 @@ public class CsIcdPathVO { @ApiModelProperty("状态,1-正常,0-删除") private Integer state; - @ApiModelProperty("MMS映射JSON") - private String jsonStr; - - @ApiModelProperty("MMS映射XML") - private String xmlStr; - @ApiModelProperty("校验结论,0-否,1-是") private Integer result; @ApiModelProperty("校验结论详情JSON") private JsonNode msg; - @ApiModelProperty("ICD类型,1-标准ICD") + @ApiModelProperty("ICD类型,1-手动录入的标准ICD,2-手动录入的非标准ICD,3-上游解析传递的标准ICD,4-上游解析传递的非标准ICD") private Integer type; @ApiModelProperty("标准ICD引用ID") diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/CsIcdPathService.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/CsIcdPathService.java index ffeae63..f2a2690 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/CsIcdPathService.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/CsIcdPathService.java @@ -1,7 +1,9 @@ package com.njcn.gather.icd.mapping.service; +import com.fasterxml.jackson.databind.JsonNode; import com.njcn.gather.icd.mapping.pojo.param.CsIcdPathParam; import com.njcn.gather.icd.mapping.pojo.param.IcdCheckResultSaveParam; +import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO; import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO; import java.util.List; @@ -13,6 +15,12 @@ public interface CsIcdPathService { List listIcdPaths(CsIcdPathParam.ListParam param); + List listReferenceIcdPaths(); + + JsonNode getIcdCheckMsg(String icdId); + + CsIcdPathDetailVO getMappingDetail(String icdId); + boolean addIcdPath(CsIcdPathParam param); boolean updateIcdPath(CsIcdPathParam.UpdateParam param); diff --git a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/CsIcdPathServiceImpl.java b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/CsIcdPathServiceImpl.java index 9f2af1e..a40125a 100644 --- a/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/CsIcdPathServiceImpl.java +++ b/tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/CsIcdPathServiceImpl.java @@ -1,13 +1,14 @@ package com.njcn.gather.icd.mapping.service.impl; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.njcn.gather.icd.mapping.mapper.CsIcdPathMapper; import com.njcn.gather.icd.mapping.pojo.param.CsIcdPathParam; import com.njcn.gather.icd.mapping.pojo.param.IcdCheckResultSaveParam; import com.njcn.gather.icd.mapping.pojo.po.CsIcdPathPO; +import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO; import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO; import com.njcn.gather.icd.mapping.service.CsIcdPathService; import com.njcn.web.utils.RequestUtil; @@ -15,6 +16,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -30,7 +32,13 @@ public class CsIcdPathServiceImpl implements CsIcdPathService { private static final int STATE_DELETED = 0; - private static final int ICD_TYPE_STANDARD = 1; + private static final int ICD_TYPE_MANUAL_STANDARD = 1; + + private static final int ICD_TYPE_MANUAL_NON_STANDARD = 2; + + private static final int ICD_TYPE_UPSTREAM_STANDARD = 3; + + private static final int ICD_TYPE_UPSTREAM_NON_STANDARD = 4; private final CsIcdPathMapper csIcdPathMapper; @@ -45,12 +53,38 @@ public class CsIcdPathServiceImpl implements CsIcdPathService { checkedParam.getResult()); } + @Override + public List listReferenceIcdPaths() { + return csIcdPathMapper.selectReferenceIcdPathList(); + } + + @Override + public JsonNode getIcdCheckMsg(String icdId) { + String id = requireText(icdId, "ICD璁板綍ID涓嶈兘涓虹┖"); + CsIcdPathVO icdPath = csIcdPathMapper.selectIcdCheckMsgById(id); + return icdPath == null ? null : icdPath.getMsg(); + } + + @Override + public CsIcdPathDetailVO getMappingDetail(String icdId) { + String id = requireText(icdId, "ICD记录ID不能为空"); + CsIcdPathDetailVO detail = csIcdPathMapper.selectIcdPathDetailById(id); + if (detail == null) { + return null; + } + byte[] icdContent = detail.getIcdContent(); + if (icdContent != null && icdContent.length > 0) { + detail.setIcdText(new String(icdContent, StandardCharsets.UTF_8)); + } + return detail; + } + @Override @Transactional public boolean addIcdPath(CsIcdPathParam param) { CsIcdPathParam checkedParam = requireParam(param); LocalDateTime now = LocalDateTime.now(); - CsIcdPathPO icdPath = buildIcdPath(checkedParam); + CsIcdPathPO icdPath = buildIcdPath(checkedParam, true); icdPath.setId(UUID.randomUUID().toString().replace("-", "")); icdPath.setState(STATE_NORMAL); icdPath.setCreateBy(currentUserId()); @@ -65,7 +99,7 @@ public class CsIcdPathServiceImpl implements CsIcdPathService { public boolean updateIcdPath(CsIcdPathParam.UpdateParam param) { CsIcdPathParam.UpdateParam checkedParam = requireUpdateParam(param); requireIcdPath(checkedParam.getId()); - CsIcdPathPO icdPath = buildIcdPath(checkedParam); + CsIcdPathPO icdPath = buildIcdPath(checkedParam, false); icdPath.setId(checkedParam.getId()); icdPath.setUpdateBy(currentUserId()); icdPath.setUpdateTime(LocalDateTime.now()); @@ -80,13 +114,21 @@ public class CsIcdPathServiceImpl implements CsIcdPathService { String currentUserId = currentUserId(); csIcdPathMapper.update(null, new LambdaUpdateWrapper() - .set(CsIcdPathPO::getType, null) + .set(CsIcdPathPO::getType, ICD_TYPE_MANUAL_NON_STANDARD) .set(CsIcdPathPO::getUpdateBy, currentUserId) .set(CsIcdPathPO::getUpdateTime, now) - .eq(CsIcdPathPO::getState, STATE_NORMAL)); + .eq(CsIcdPathPO::getState, STATE_NORMAL) + .eq(CsIcdPathPO::getType, ICD_TYPE_MANUAL_STANDARD)); + + csIcdPathMapper.update(null, new LambdaUpdateWrapper() + .set(CsIcdPathPO::getType, ICD_TYPE_UPSTREAM_NON_STANDARD) + .set(CsIcdPathPO::getUpdateBy, currentUserId) + .set(CsIcdPathPO::getUpdateTime, now) + .eq(CsIcdPathPO::getState, STATE_NORMAL) + .eq(CsIcdPathPO::getType, ICD_TYPE_UPSTREAM_STANDARD)); CsIcdPathPO activeIcdPath = new CsIcdPathPO(); - activeIcdPath.setType(ICD_TYPE_STANDARD); + activeIcdPath.setType(resolveStandardType(targetIcdPath.getType())); activeIcdPath.setUpdateBy(currentUserId); activeIcdPath.setUpdateTime(now); return csIcdPathMapper.update(activeIcdPath, new LambdaUpdateWrapper() @@ -116,7 +158,7 @@ public class CsIcdPathServiceImpl implements CsIcdPathService { throw new IllegalArgumentException("ICD校验结果不能为空"); } CsIcdPathPO icdPath = requireIcdPath(icdId); - CsIcdPathPO referenceIcd = requireUniqueReferenceIcd(); + CsIcdPathPO referenceIcd = requireReferenceIcd(icdPath.getReferenceIcdId()); icdPath.setJsonStr(trimToNull(param.getMappingJson())); icdPath.setXmlStr(trimToNull(param.getXml())); icdPath.setResult(normalizeResult(param.getResult())); @@ -140,17 +182,34 @@ public class CsIcdPathServiceImpl implements CsIcdPathService { } } - private CsIcdPathPO buildIcdPath(CsIcdPathParam param) { + private CsIcdPathPO buildIcdPath(CsIcdPathParam param, boolean useDefaultType) { CsIcdPathPO icdPath = new CsIcdPathPO(); icdPath.setName(requireText(param.getName(), "ICD名称不能为空")); - icdPath.setPath(requireText(param.getPath(), "ICD存储路径不能为空")); icdPath.setIcdContent(param.getIcdContent()); icdPath.setAngle(param.getAngle()); icdPath.setUsePhaseIndex(param.getUsePhaseIndex()); - icdPath.setType(param.getType()); + icdPath.setType(useDefaultType ? resolveIcdType(param.getType()) : param.getType()); return icdPath; } + /** + * 新增 ICD 记录未显式传类型时,默认归类为手动录入的非标准 ICD。 + */ + private Integer resolveIcdType(Integer type) { + return type == null ? ICD_TYPE_MANUAL_NON_STANDARD : type; + } + + /** + * 激活标准 ICD 时保留记录来源:手动录入升为 1,上游解析传递升为 3。 + */ + private Integer resolveStandardType(Integer type) { + if (Integer.valueOf(ICD_TYPE_UPSTREAM_STANDARD).equals(type) + || Integer.valueOf(ICD_TYPE_UPSTREAM_NON_STANDARD).equals(type)) { + return ICD_TYPE_UPSTREAM_STANDARD; + } + return ICD_TYPE_MANUAL_STANDARD; + } + private CsIcdPathParam requireParam(CsIcdPathParam param) { if (param == null) { throw new IllegalArgumentException("ICD记录参数不能为空"); @@ -176,19 +235,15 @@ public class CsIcdPathServiceImpl implements CsIcdPathService { } /** - * 全系统只允许一个正常状态的标准 ICD 作为唯一参照。 + * ICD 校验保存时以当前记录绑定的 Reference_Icd_Id 作为参照来源。 */ - private CsIcdPathPO requireUniqueReferenceIcd() { - List referenceIcdList = csIcdPathMapper.selectList(new LambdaQueryWrapper() - .eq(CsIcdPathPO::getState, STATE_NORMAL) - .eq(CsIcdPathPO::getType, ICD_TYPE_STANDARD)); - if (referenceIcdList == null || referenceIcdList.isEmpty()) { - throw new IllegalArgumentException("未配置标准ICD,无法执行唯一性校验"); + private CsIcdPathPO requireReferenceIcd(String referenceIcdId) { + String id = requireText(referenceIcdId, "未配置参照ICD,无法保存校验结果"); + CsIcdPathPO referenceIcd = csIcdPathMapper.selectById(id); + if (referenceIcd == null || !Integer.valueOf(STATE_NORMAL).equals(referenceIcd.getState())) { + throw new IllegalArgumentException("参照ICD不存在或已删除,无法保存校验结果"); } - if (referenceIcdList.size() > 1) { - throw new IllegalArgumentException("存在多个标准ICD,无法确定唯一参照"); - } - return referenceIcdList.get(0); + return referenceIcd; } private Integer normalizeResult(Integer result) { diff --git a/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/component/IcdConsistencyCheckServiceTest.java b/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/component/IcdConsistencyCheckServiceTest.java index a00ecb5..f143f6d 100644 --- a/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/component/IcdConsistencyCheckServiceTest.java +++ b/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/component/IcdConsistencyCheckServiceTest.java @@ -36,20 +36,62 @@ class IcdConsistencyCheckServiceTest { } @Test - void checkShouldOnlyReturnCorrectedJsonForRtFreSelfMappingRule() { + void checkShouldReturnPassAndCorrectedJsonForRtFreSelfMappingRule() { IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest(); request.setStandardJson(buildRtFreStandardJson()); request.setCheckedJson(buildRtFreCheckedJson()); IcdConsistencyCheckResponse response = service.check(request); - Assertions.assertEquals(0, response.getResult()); + Assertions.assertEquals(1, response.getResult()); + Assertions.assertTrue(response.getIssues().isEmpty()); Assertions.assertTrue(response.getCorrectedJson().contains("\"rptID\" : \"demoRtFre\"")); Assertions.assertTrue(response.getCorrectedJson().contains("\"FlickerFlag\" : \"1\"")); Assertions.assertTrue(response.getCorrectedJson().contains("\"reportCount\" : 0")); Assertions.assertTrue(response.getCorrectedJson().contains("\"reportCount\" : 1")); } + @Test + void checkShouldNotCorrectRtFreWhenReportMapAlreadyMatchesStandard() { + IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest(); + request.setStandardJson(buildRtFreStandardJson()); + request.setCheckedJson(buildRtFreStandardJson()); + + IcdConsistencyCheckResponse response = service.check(request); + + Assertions.assertEquals(1, response.getResult()); + Assertions.assertNull(response.getCorrectedJson()); + } + + @Test + void checkShouldKeepStandardDoiWhenSameDoiListContainsDuplicateDesc() { + IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest(); + request.setStandardJson(buildStandardJson()); + request.setCheckedJson(buildDuplicateDoiDescJson()); + + IcdConsistencyCheckResponse response = service.check(request); + + Assertions.assertEquals(0, response.getResult()); + Assertions.assertTrue(response.getIssuesJson().contains("同一个 doiList 下存在 desc 相同的指标")); + Assertions.assertTrue(response.getIssuesJson().contains("频率+Hz 在标准映射中存在")); + Assertions.assertTrue(response.getIssuesJson().contains("频率+Hz2 不在标准映射中")); + Assertions.assertNotNull(response.getCorrectedJson()); + Assertions.assertTrue(response.getCorrectedJson().contains("\"name\" : \"Hz\"")); + Assertions.assertFalse(response.getCorrectedJson().contains("\"name\" : \"Hz2\"")); + } + + @Test + void checkShouldIgnoreVoltageStartMetricsDuringDoiConsistency() { + IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest(); + request.setStandardJson(buildVoltageStartMetricsStandardJson()); + request.setCheckedJson(buildStandardJson()); + + IcdConsistencyCheckResponse response = service.check(request); + + Assertions.assertEquals(1, response.getResult()); + Assertions.assertTrue(response.getIssues().isEmpty()); + } + @Test void checkShouldReportEmptySdiListAsTypeListProblem() { IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest(); @@ -63,6 +105,18 @@ class IcdConsistencyCheckServiceTest { Assertions.assertFalse(response.getIssuesJson().contains("sdiList 不能为空")); } + @Test + void checkShouldIgnoreVoltageStartMetricsDuringSelfFormatTypeListValidation() { + IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest(); + request.setStandardJson(buildVoltageStartMetricsWithoutSdiJson()); + request.setCheckedJson(buildVoltageStartMetricsWithoutSdiJson()); + + IcdConsistencyCheckResponse response = service.check(request); + + Assertions.assertEquals(1, response.getResult()); + Assertions.assertTrue(response.getIssues().isEmpty()); + } + @Test void checkShouldReturnPassWhenCheckedJsonMatchesStandardJson() { IcdConsistencyCheckRequest request = new IcdConsistencyCheckRequest(); @@ -161,6 +215,69 @@ class IcdConsistencyCheckServiceTest { "}"; } + private String buildDuplicateDoiDescJson() { + return "{\n" + + " \"IED\":\"IED1\",\n" + + " \"LD\":\"LD0\",\n" + + " \"DataType\":\"1\",\n" + + " \"unit\":\"s\",\n" + + " \"ReportMap\":[\n" + + " {\"desc\":\"统计数据\",\"reportCount\":2,\"rptID\":\"rpt-stat\",\"name\":\"brcbStat\",\"buffered\":\"BR\",\"inst\":\"01\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" + + " {\"desc\":\"波动闪变\",\"reportCount\":1,\"rptID\":\"rpt-flk\",\"name\":\"brcbFlk\",\"buffered\":\"BR\",\"inst\":\"02\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" + + " {\"desc\":\"实时数据\",\"reportCount\":1,\"rptID\":\"rpt-rt\",\"name\":\"brcbRt\",\"buffered\":\"RP\",\"inst\":\"03\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" + + " {\"desc\":\"暂态事件\",\"reportCount\":1,\"rptID\":\"rpt-tran\",\"name\":\"brcbTran\",\"buffered\":\"BR\",\"inst\":\"04\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"}\n" + + " ],\n" + + " \"DataSetList\":[\n" + + buildDataSetWithDuplicateDoiDesc("MMXU", "统计数据", "1", "A相") + ",\n" + + buildDataSet("MSQI", "实时数据", "1", "A相", "A", "电流", 1, 2, "A") + ",\n" + + buildDataSet("MHAI", "谐波数据", "1", "A相", "Har", "谐波", 1, 2, "%") + ",\n" + + buildDataSet("MFLK", "波动闪变", "1", "A相", "Flk", "闪变", 1, 2, "pu") + "\n" + + " ]\n" + + "}"; + } + + private String buildVoltageStartMetricsStandardJson() { + return "{\n" + + " \"IED\":\"IED1\",\n" + + " \"LD\":\"LD0\",\n" + + " \"DataType\":\"1\",\n" + + " \"unit\":\"s\",\n" + + " \"ReportMap\":[\n" + + " {\"desc\":\"统计数据\",\"reportCount\":2,\"rptID\":\"rpt-stat\",\"name\":\"brcbStat\",\"buffered\":\"BR\",\"inst\":\"01\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" + + " {\"desc\":\"波动闪变\",\"reportCount\":1,\"rptID\":\"rpt-flk\",\"name\":\"brcbFlk\",\"buffered\":\"BR\",\"inst\":\"02\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" + + " {\"desc\":\"实时数据\",\"reportCount\":1,\"rptID\":\"rpt-rt\",\"name\":\"brcbRt\",\"buffered\":\"RP\",\"inst\":\"03\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" + + " {\"desc\":\"暂态事件\",\"reportCount\":1,\"rptID\":\"rpt-tran\",\"name\":\"brcbTran\",\"buffered\":\"BR\",\"inst\":\"04\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"}\n" + + " ],\n" + + " \"DataSetList\":[\n" + + buildDataSetWithVoltageStartMetrics("MMXU", "统计数据", "1", "A相") + ",\n" + + buildDataSet("MSQI", "实时数据", "1", "A相", "A", "电流", 1, 2, "A") + ",\n" + + buildDataSet("MHAI", "谐波数据", "1", "A相", "Har", "谐波", 1, 2, "%") + ",\n" + + buildDataSet("MFLK", "波动闪变", "1", "A相", "Flk", "闪变", 1, 2, "pu") + "\n" + + " ]\n" + + "}"; + } + + private String buildVoltageStartMetricsWithoutSdiJson() { + return "{\n" + + " \"IED\":\"IED1\",\n" + + " \"LD\":\"LD0\",\n" + + " \"DataType\":\"1\",\n" + + " \"unit\":\"s\",\n" + + " \"ReportMap\":[\n" + + " {\"desc\":\"统计数据\",\"reportCount\":2,\"rptID\":\"rpt-stat\",\"name\":\"brcbStat\",\"buffered\":\"BR\",\"inst\":\"01\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" + + " {\"desc\":\"波动闪变\",\"reportCount\":1,\"rptID\":\"rpt-flk\",\"name\":\"brcbFlk\",\"buffered\":\"BR\",\"inst\":\"02\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" + + " {\"desc\":\"实时数据\",\"reportCount\":1,\"rptID\":\"rpt-rt\",\"name\":\"brcbRt\",\"buffered\":\"RP\",\"inst\":\"03\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"},\n" + + " {\"desc\":\"暂态事件\",\"reportCount\":1,\"rptID\":\"rpt-tran\",\"name\":\"brcbTran\",\"buffered\":\"BR\",\"inst\":\"04\",\"FlickerFlag\":\"0\",\"Select\":\"all\",\"TrgOps\":\"dchg\"}\n" + + " ],\n" + + " \"DataSetList\":[\n" + + buildDataSetWithVoltageStartMetricsWithoutSdi("MMXU", "统计数据", "1", "A相") + ",\n" + + buildDataSet("MSQI", "实时数据", "1", "A相", "A", "电流", 1, 2, "A") + ",\n" + + buildDataSet("MHAI", "谐波数据", "1", "A相", "Har", "谐波", 1, 2, "%") + ",\n" + + buildDataSet("MFLK", "波动闪变", "1", "A相", "Flk", "闪变", 1, 2, "pu") + "\n" + + " ]\n" + + "}"; + } + private String buildEmptySdiListJson() { return "{\n" + " \"IED\":\"IED1\",\n" + @@ -196,4 +313,51 @@ class IcdConsistencyCheckServiceTest { "\",\"desc\":\"" + instDesc + "\",\"doiList\":[{\"name\":\"" + doiName + "\",\"desc\":\"" + doiDesc + "\",\"start\":1,\"end\":4,\"unit\":\"Hz\",\"coefficient\":1.0,\"baseflag\":1,\"basecount\":1,\"icdcout\":10,\"sdiList\":[]}]}]}"; } + + private String buildDataSetWithDuplicateDoiDesc(String lnClass, String groupDesc, String inst, String instDesc) { + return " {\"desc\":\"" + groupDesc + "\",\"lnClass\":\"" + lnClass + "\",\"instList\":[{\"inst\":\"" + inst + + "\",\"desc\":\"" + instDesc + "\",\"doiList\":[" + + buildDoi("Hz2", "频率", 5, 8, "Hz") + "," + + buildDoi("Hz", "频率", 1, 4, "Hz") + + "]}]}"; + } + + private String buildDataSetWithVoltageStartMetrics(String lnClass, String groupDesc, String inst, String instDesc) { + return " {\"desc\":\"" + groupDesc + "\",\"lnClass\":\"" + lnClass + "\",\"instList\":[{\"inst\":\"" + inst + + "\",\"desc\":\"" + instDesc + "\",\"doiList\":[" + + buildDoi("Hz", "频率", 1, 4, "Hz") + "," + + buildDoi("VolDistStr", "电压扰动事件启动", 5, 6, "") + "," + + buildDoi("VolDipStr", "电压暂降事件启动", 7, 8, "") + "," + + buildDoi("VolSwellStr", "电压暂升事件启动", 9, 10, "") + "," + + buildDoi("VolInterStr", "电压中断事件启动", 11, 12, "") + "," + + buildDoi("VolDipSet", "电压暂降启动定值", 13, 14, "V") + "," + + buildDoi("VolSwellSet", "电压暂升启动定值", 15, 16, "V") + "," + + buildDoi("VolInterSet", "电压中断启动定值", 17, 18, "V") + + "]}]}"; + } + + private String buildDataSetWithVoltageStartMetricsWithoutSdi(String lnClass, String groupDesc, String inst, String instDesc) { + return " {\"desc\":\"" + groupDesc + "\",\"lnClass\":\"" + lnClass + "\",\"instList\":[{\"inst\":\"" + inst + + "\",\"desc\":\"" + instDesc + "\",\"doiList\":[" + + buildDoiWithoutSdi("VolDistStr", "电压扰动事件启动", 5, 6, "") + "," + + buildDoiWithoutSdi("VolDipStr", "电压暂降事件启动", 7, 8, "") + "," + + buildDoiWithoutSdi("VolSwellStr", "电压暂升事件启动", 9, 10, "") + "," + + buildDoiWithoutSdi("VolInterStr", "电压中断事件启动", 11, 12, "") + "," + + buildDoiWithoutSdi("VolDipSet", "电压暂降启动定值", 13, 14, "V") + "," + + buildDoiWithoutSdi("VolSwellSet", "电压暂升启动定值", 15, 16, "V") + "," + + buildDoiWithoutSdi("VolInterSet", "电压中断启动定值", 17, 18, "V") + + "]}]}"; + } + + private String buildDoi(String doiName, String doiDesc, int start, int end, String unit) { + return "{\"name\":\"" + doiName + "\",\"desc\":\"" + doiDesc + + "\",\"start\":" + start + ",\"end\":" + end + ",\"unit\":\"" + unit + + "\",\"coefficient\":1.0,\"baseflag\":1,\"basecount\":1,\"icdcout\":10,\"sdiList\":[{\"name\":\"mag\",\"desc\":\"幅值\",\"typeList\":[{\"name\":\"f\",\"desc\":\"浮点\"}]}]}"; + } + + private String buildDoiWithoutSdi(String doiName, String doiDesc, int start, int end, String unit) { + return "{\"name\":\"" + doiName + "\",\"desc\":\"" + doiDesc + + "\",\"start\":" + start + ",\"end\":" + end + ",\"unit\":\"" + unit + + "\",\"coefficient\":1.0,\"baseflag\":1,\"basecount\":1,\"icdcout\":10,\"sdiList\":[]}"; + } } diff --git a/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/service/impl/CsIcdPathServiceImplTest.java b/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/service/impl/CsIcdPathServiceImplTest.java index 83aa585..dae9f4f 100644 --- a/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/service/impl/CsIcdPathServiceImplTest.java +++ b/tools/mms-mapping/src/test/java/com/njcn/gather/icd/mapping/service/impl/CsIcdPathServiceImplTest.java @@ -5,6 +5,8 @@ import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument; import com.njcn.gather.icd.mapping.pojo.param.CsIcdPathParam; import com.njcn.gather.icd.mapping.pojo.param.IcdCheckResultSaveParam; import com.njcn.gather.icd.mapping.pojo.po.CsIcdPathPO; +import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathDetailVO; +import com.njcn.gather.icd.mapping.pojo.vo.CsIcdPathVO; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Assertions; @@ -47,6 +49,26 @@ class CsIcdPathServiceImplTest { verify(csIcdPathMapper).selectIcdPathList(eq("standard"), eq(null), eq(null)); } + @Test + void listReferenceIcdPathsShouldQueryStandardIcdTypes() { + service.listReferenceIcdPaths(); + + verify(csIcdPathMapper).selectReferenceIcdPathList(); + } + + @Test + void getIcdCheckMsgShouldQueryMsgByTrimmedId() { + JsonNode msg = objectMapper.createObjectNode().put("summary", "通过"); + CsIcdPathVO vo = new CsIcdPathVO(); + vo.setMsg(msg); + when(csIcdPathMapper.selectIcdCheckMsgById(eq("icd-001"))).thenReturn(vo); + + JsonNode result = service.getIcdCheckMsg(" icd-001 "); + + Assertions.assertSame(msg, result); + verify(csIcdPathMapper).selectIcdCheckMsgById(eq("icd-001")); + } + @Test void addIcdPathShouldInsertEnabledRecord() { CsIcdPathParam param = buildParam("标准ICD"); @@ -58,7 +80,6 @@ class CsIcdPathServiceImplTest { verify(csIcdPathMapper).insert(captor.capture()); Assertions.assertTrue(result); Assertions.assertEquals("标准ICD", captor.getValue().getName()); - Assertions.assertEquals("D:/icd/standard.icd", captor.getValue().getPath()); Assertions.assertEquals(1, captor.getValue().getState()); Assertions.assertNotNull(captor.getValue().getId()); Assertions.assertNotNull(captor.getValue().getCreateTime()); @@ -79,12 +100,25 @@ class CsIcdPathServiceImplTest { Assertions.assertArrayEquals(fileContent, captor.getValue().getIcdContent()); } + @Test + void addIcdPathShouldDefaultTypeToManualNonStandard() { + CsIcdPathParam param = buildParam("手动录入非标准ICD"); + param.setType(null); + when(csIcdPathMapper.insert(any(CsIcdPathPO.class))).thenReturn(1); + + boolean result = service.addIcdPath(param); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CsIcdPathPO.class); + verify(csIcdPathMapper).insert(captor.capture()); + Assertions.assertTrue(result); + Assertions.assertEquals(2, captor.getValue().getType()); + } + @Test void updateIcdPathShouldRejectDeletedRecord() { CsIcdPathParam.UpdateParam param = new CsIcdPathParam.UpdateParam(); param.setId("icd-001"); param.setName("标准ICD"); - param.setPath("D:/icd/standard.icd"); CsIcdPathPO deleted = new CsIcdPathPO(); deleted.setId("icd-001"); @@ -103,7 +137,6 @@ class CsIcdPathServiceImplTest { byte[] fileContent = "".getBytes(); param.setId("icd-001"); param.setName("标准ICD"); - param.setPath("standard.icd"); param.setIcdContent(fileContent); CsIcdPathPO existed = new CsIcdPathPO(); @@ -134,21 +167,40 @@ class CsIcdPathServiceImplTest { } @Test - void activateIcdPathShouldOnlyKeepTargetAsStandardIcd() { + void activateIcdPathShouldOnlyKeepTargetAsManualStandardIcd() { CsIcdPathPO icdPath = new CsIcdPathPO(); icdPath.setId("icd-001"); icdPath.setState(1); + icdPath.setType(2); when(csIcdPathMapper.selectById(eq("icd-001"))).thenReturn(icdPath); when(csIcdPathMapper.update(any(CsIcdPathPO.class), any())).thenReturn(1); boolean result = service.activateIcdPath("icd-001"); ArgumentCaptor captor = ArgumentCaptor.forClass(CsIcdPathPO.class); - verify(csIcdPathMapper, times(2)).update(captor.capture(), any()); + verify(csIcdPathMapper, times(3)).update(captor.capture(), any()); Assertions.assertTrue(result); Assertions.assertNull(captor.getAllValues().get(0)); - Assertions.assertEquals(1, captor.getAllValues().get(1).getType()); - Assertions.assertNotNull(captor.getAllValues().get(1).getUpdateTime()); + Assertions.assertNull(captor.getAllValues().get(1)); + Assertions.assertEquals(1, captor.getAllValues().get(2).getType()); + Assertions.assertNotNull(captor.getAllValues().get(2).getUpdateTime()); + } + + @Test + void activateIcdPathShouldKeepUpstreamSourceWhenTargetIsUpstreamIcd() { + CsIcdPathPO icdPath = new CsIcdPathPO(); + icdPath.setId("icd-001"); + icdPath.setState(1); + icdPath.setType(4); + when(csIcdPathMapper.selectById(eq("icd-001"))).thenReturn(icdPath); + when(csIcdPathMapper.update(any(CsIcdPathPO.class), any())).thenReturn(1); + + boolean result = service.activateIcdPath("icd-001"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CsIcdPathPO.class); + verify(csIcdPathMapper, times(3)).update(captor.capture(), any()); + Assertions.assertTrue(result); + Assertions.assertEquals(3, captor.getAllValues().get(2).getType()); } @Test @@ -235,10 +287,36 @@ class CsIcdPathServiceImplTest { Assertions.assertArrayEquals(objectMapper.writeValueAsBytes(icdDocument), captor.getValue().getIcdContent()); } + @Test + void getMappingDetailShouldReturnJsonXmlAndUtf8IcdText() { + CsIcdPathDetailVO detail = new CsIcdPathDetailVO(); + detail.setId("icd-001"); + detail.setName("标准ICD"); + detail.setJsonStr("{\"ied\":\"IED1\"}"); + detail.setXmlStr(""); + detail.setIcdContent("".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + when(csIcdPathMapper.selectIcdPathDetailById(eq("icd-001"))).thenReturn(detail); + + CsIcdPathDetailVO result = service.getMappingDetail(" icd-001 "); + + Assertions.assertEquals("{\"ied\":\"IED1\"}", result.getJsonStr()); + Assertions.assertEquals("", result.getXmlStr()); + Assertions.assertEquals("", result.getIcdText()); + verify(csIcdPathMapper).selectIcdPathDetailById(eq("icd-001")); + } + + @Test + void getMappingDetailShouldReturnNullWhenRecordMissing() { + when(csIcdPathMapper.selectIcdPathDetailById(eq("missing"))).thenReturn(null); + + CsIcdPathDetailVO result = service.getMappingDetail("missing"); + + Assertions.assertNull(result); + } + private CsIcdPathParam buildParam(String name) { CsIcdPathParam param = new CsIcdPathParam(); param.setName(name); - param.setPath("D:/icd/standard.icd"); param.setAngle(0); param.setUsePhaseIndex(1); param.setType(1); diff --git a/tools/parse-pqdif/pom.xml b/tools/parse-pqdif/pom.xml index f06d4f3..8c7a5b3 100644 --- a/tools/parse-pqdif/pom.xml +++ b/tools/parse-pqdif/pom.xml @@ -20,6 +20,12 @@ 0.0.1 + + com.njcn + mybatis-plus + 0.0.1 + + com.njcn spingboot2.3.12 @@ -74,6 +80,14 @@ pqdif-samples/** + + + + src/main/java + + **/*.xml + + diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/CsPqdifPathController.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/CsPqdifPathController.java new file mode 100644 index 0000000..bede4a3 --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/CsPqdifPathController.java @@ -0,0 +1,162 @@ +package com.njcn.gather.tool.parsepqdif.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.tool.parsepqdif.pojo.param.CsPqdifPathParam; +import com.njcn.gather.tool.parsepqdif.pojo.param.PqdifParseResultSaveParam; +import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO; +import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathVO; +import com.njcn.gather.tool.parsepqdif.service.CsPqdifPathService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +/** + * PQDIF 存储记录维护入口。 + */ +@Slf4j +@Api(tags = "PQDIF存储记录管理") +@RestController +@RequestMapping("/api/parse-pqdif/pqdif-paths") +@RequiredArgsConstructor +public class CsPqdifPathController extends BaseController { + + private final CsPqdifPathService csPqdifPathService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询PQDIF存储记录列表") + @PostMapping("/list") + public HttpResult> list(@RequestBody(required = false) CsPqdifPathParam.ListParam param) { + String methodDescribe = getMethodDescribe("list"); + LogUtil.njcnDebug(log, "{},开始查询PQDIF存储记录列表", methodDescribe); + List result = csPqdifPathService.listPqdifPaths(param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("新增PQDIF存储记录") + @PostMapping(value = "/add", consumes = {"application/json"}) + public HttpResult add(@RequestBody @Validated CsPqdifPathParam param) { + String methodDescribe = getMethodDescribe("add"); + LogUtil.njcnDebug(log, "{},开始新增PQDIF存储记录", methodDescribe); + boolean result = csPqdifPathService.addPqdifPath(param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("上传并新增PQDIF存储记录") + @PostMapping(value = "/add", consumes = {"multipart/form-data"}) + public HttpResult addWithFile(@RequestPart("pqdifFile") MultipartFile pqdifFile, + @RequestPart("request") @Validated CsPqdifPathParam param) { + String methodDescribe = getMethodDescribe("addWithFile"); + LogUtil.njcnDebug(log, "{},开始上传并新增PQDIF存储记录,fileName={}", methodDescribe, resolveFileName(pqdifFile)); + fillPqdifFile(param, pqdifFile); + boolean result = csPqdifPathService.addPqdifPath(param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("编辑PQDIF存储记录") + @PostMapping(value = "/update", consumes = {"application/json"}) + public HttpResult update(@RequestBody @Validated CsPqdifPathParam.UpdateParam param) { + String methodDescribe = getMethodDescribe("update"); + LogUtil.njcnDebug(log, "{},开始编辑PQDIF存储记录,pqdifId={}", methodDescribe, param.getId()); + boolean result = csPqdifPathService.updatePqdifPath(param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("上传并编辑PQDIF存储记录") + @PostMapping(value = "/update", consumes = {"multipart/form-data"}) + public HttpResult updateWithFile(@RequestPart("pqdifFile") MultipartFile pqdifFile, + @RequestPart("request") @Validated CsPqdifPathParam.UpdateParam param) { + String methodDescribe = getMethodDescribe("updateWithFile"); + LogUtil.njcnDebug(log, "{},开始上传并编辑PQDIF存储记录,pqdifId={},fileName={}", + methodDescribe, param.getId(), resolveFileName(pqdifFile)); + fillPqdifFile(param, pqdifFile); + boolean result = csPqdifPathService.updatePqdifPath(param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("删除PQDIF存储记录") + @PostMapping("/delete") + public HttpResult delete(@RequestBody List ids) { + String methodDescribe = getMethodDescribe("delete"); + LogUtil.njcnDebug(log, "{},开始删除PQDIF存储记录,ids={}", methodDescribe, ids); + boolean result = csPqdifPathService.deletePqdifPath(ids); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询PQDIF解析结果详情") + @ApiImplicitParam(name = "id", value = "PQDIF记录ID", required = true) + @PostMapping("/{id}/parse-msg") + public HttpResult getPqdifParseMsg(@PathVariable("id") String id) { + String methodDescribe = getMethodDescribe("getPqdifParseMsg"); + LogUtil.njcnDebug(log, "{},开始查询PQDIF解析结果详情,pqdifId={}", methodDescribe, id); + JsonNode result = csPqdifPathService.getPqdifParseMsg(id); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询PQDIF文件和解析结果详情") + @ApiImplicitParam(name = "id", value = "PQDIF记录ID", required = true) + @PostMapping("/{id}/parse-detail") + public HttpResult getPqdifParseDetail(@PathVariable("id") String id) { + String methodDescribe = getMethodDescribe("getPqdifParseDetail"); + LogUtil.njcnDebug(log, "{},开始查询PQDIF文件和解析结果详情,pqdifId={}", methodDescribe, id); + CsPqdifPathDetailVO result = csPqdifPathService.getPqdifParseDetail(id); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("保存PQDIF解析结果") + @ApiImplicitParam(name = "id", value = "PQDIF记录ID", required = true) + @PostMapping(value = "/{id}/parse-result", consumes = {"application/json"}) + public HttpResult savePqdifParseResult(@PathVariable("id") String id, + @RequestBody PqdifParseResultSaveParam param) { + String methodDescribe = getMethodDescribe("savePqdifParseResult"); + LogUtil.njcnDebug(log, "{},开始保存PQDIF解析结果,pqdifId={}", methodDescribe, id); + boolean result = csPqdifPathService.savePqdifParseResult(id, param); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); + } + + private void fillPqdifFile(CsPqdifPathParam param, MultipartFile pqdifFile) { + if (pqdifFile == null || pqdifFile.isEmpty()) { + throw new IllegalArgumentException("PQDIF文件不能为空"); + } + try { + param.setPqdifContent(pqdifFile.getBytes()); + } catch (IOException ex) { + throw new IllegalArgumentException("读取PQDIF文件失败:" + ex.getMessage(), ex); + } + } + + private String resolveFileName(MultipartFile pqdifFile) { + if (pqdifFile == null || pqdifFile.getOriginalFilename() == null) { + return null; + } + String fileName = pqdifFile.getOriginalFilename().trim(); + return fileName.isEmpty() ? null : fileName; + } +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java index 997b494..4983927 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/ParsePqdifController.java @@ -28,7 +28,7 @@ import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api/parse-pqdif") @RequiredArgsConstructor -public class ParsePqdifController extends BaseController { +public class ParsePqdifController extends BaseController { private final ParsePqdifService parsePqdifService; @@ -38,7 +38,7 @@ public class ParsePqdifController extends BaseController { @PostMapping(value = "/parse", consumes = {"multipart/form-data"}) public HttpResult parse(@RequestPart("pqdifFile") MultipartFile pqdifFile) { String methodDescribe = getMethodDescribe("parse"); - LogUtil.njcnDebug(log, "{},PQDIF解析预留入口,fileName={}", + LogUtil.njcnDebug(log, "{},PQDIF解析入口,fileName={}", methodDescribe, pqdifFile == null ? null : pqdifFile.getOriginalFilename()); PqdifParseResponse result = parsePqdifService.parse(pqdifFile); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/mapper/CsPqdifPathMapper.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/mapper/CsPqdifPathMapper.java new file mode 100644 index 0000000..5ccd2ac --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/mapper/CsPqdifPathMapper.java @@ -0,0 +1,19 @@ +package com.njcn.gather.tool.parsepqdif.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.tool.parsepqdif.pojo.po.CsPqdifPathPO; +import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO; +import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface CsPqdifPathMapper extends BaseMapper { + + List selectPqdifPathList(@Param("keyword") String keyword, + @Param("result") Integer result); + + CsPqdifPathVO selectPqdifParseMsgById(@Param("id") String id); + + CsPqdifPathDetailVO selectPqdifPathDetailById(@Param("id") String id); +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/mapper/mapping/CsPqdifPathMapper.xml b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/mapper/mapping/CsPqdifPathMapper.xml new file mode 100644 index 0000000..7ca0c12 --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/mapper/mapping/CsPqdifPathMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java index a40bf95..7780f4b 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/nativebridge/PqdifNativeLibraryLoader.java @@ -1,12 +1,17 @@ package com.njcn.gather.tool.parsepqdif.nativebridge; import com.sun.jna.NativeLibrary; +import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.io.InputStream; -import java.nio.file.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.Locale; +@Slf4j public final class PqdifNativeLibraryLoader { private static final String RESOURCE_DLL = "/pqdif-native/win-x64/pqdifbasic.dll"; @@ -41,8 +46,8 @@ public final class PqdifNativeLibraryLoader { NativeLibrary.addSearchPath("pqdifbasic", nativeDir.toAbsolutePath().toString()); NativeLibrary.addSearchPath("pqdifbasic.dll", nativeDir.toAbsolutePath().toString()); - System.out.println("PQDIF native dir = " + nativeDir.toAbsolutePath()); - System.out.println("PQDIF native dll = " + dllPath.toAbsolutePath()); + log.info("PQDIF native dir = {}", nativeDir.toAbsolutePath()); + log.info("PQDIF native dll = {}", dllPath.toAbsolutePath()); preparedNativeDir = nativeDir; prepared = true; @@ -81,4 +86,4 @@ public final class PqdifNativeLibraryLoader { System.setProperty(propertyName, nativePath + separator + oldValue); } } -} \ No newline at end of file +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/param/CsPqdifPathParam.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/param/CsPqdifPathParam.java new file mode 100644 index 0000000..9a24bcb --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/param/CsPqdifPathParam.java @@ -0,0 +1,50 @@ +package com.njcn.gather.tool.parsepqdif.pojo.param; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; + +/** + * PQDIF 存储记录保存参数。 + */ +@Data +@ApiModel("PQDIF存储记录保存参数") +public class CsPqdifPathParam { + + @ApiModelProperty("PQDIF名称") + @NotBlank(message = "PQDIF名称不能为空") + private String name; + + @ApiModelProperty("PQDIF文件二进制内容") + private byte[] pqdifContent; + + /** + * PQDIF 存储记录编辑参数。 + */ + @Data + @EqualsAndHashCode(callSuper = true) + @ApiModel("PQDIF存储记录编辑参数") + public static class UpdateParam extends CsPqdifPathParam { + + @ApiModelProperty("PQDIF记录ID") + @NotBlank(message = "PQDIF记录ID不能为空") + private String id; + } + + /** + * PQDIF 存储记录列表查询参数。 + */ + @Data + @ApiModel("PQDIF存储记录列表查询参数") + public static class ListParam { + + @ApiModelProperty("关键字,匹配PQDIF名称") + private String keyword; + + @ApiModelProperty("解析结果:1-成功,0-失败") + private Integer result; + } +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/param/PqdifParseResultSaveParam.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/param/PqdifParseResultSaveParam.java new file mode 100644 index 0000000..de79c31 --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/param/PqdifParseResultSaveParam.java @@ -0,0 +1,35 @@ +package com.njcn.gather.tool.parsepqdif.pojo.param; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * PQDIF 解析结果保存参数。 + */ +@Data +@ApiModel("PQDIF解析结果保存参数") +public class PqdifParseResultSaveParam { + + @ApiModelProperty("native解析库版本") + private String nativeVersion; + + @ApiModelProperty("Record总数") + private Long recordCount; + + @ApiModelProperty("Observation Record总数") + private Long observationCount; + + @ApiModelProperty("每个Series返回的样例采样值数量") + private Integer sampleValueCount; + + @ApiModelProperty("解析结果:1-成功,0-失败") + private Integer result; + + @ApiModelProperty("解析提示、失败原因或解析结论JSON") + private JsonNode msg; + + @ApiModelProperty("完整PQDIF解析结果JSON") + private String jsonStr; +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/po/CsPqdifPathPO.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/po/CsPqdifPathPO.java new file mode 100644 index 0000000..0ab31cb --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/po/CsPqdifPathPO.java @@ -0,0 +1,66 @@ +package com.njcn.gather.tool.parsepqdif.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.databind.JsonNode; +import com.njcn.gather.tool.parsepqdif.typehandler.JsonNodeTypeHandler; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * PQDIF 文件存储和解析结果记录。 + */ +@Data +@TableName(value = "cs_pqdif_path", autoResultMap = true) +public class CsPqdifPathPO implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId("ID") + private String id; + + @TableField("Name") + private String name; + + @TableField("Pqdif") + private byte[] pqdifContent; + + @TableField("Native_Version") + private String nativeVersion; + + @TableField("Record_Count") + private Long recordCount; + + @TableField("Observation_Count") + private Long observationCount; + + @TableField("Sample_Value_Count") + private Integer sampleValueCount; + + @TableField("Result") + private Integer result; + + @TableField(value = "Msg", typeHandler = JsonNodeTypeHandler.class) + private JsonNode msg; + + @TableField("Json_Str") + private String jsonStr; + + @TableField("State") + private Integer state; + + @TableField("Create_By") + private String createBy; + + @TableField("Create_Time") + private LocalDateTime createTime; + + @TableField("Update_By") + private String updateBy; + + @TableField("Update_Time") + private LocalDateTime updateTime; +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathDetailVO.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathDetailVO.java new file mode 100644 index 0000000..0b10ed9 --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathDetailVO.java @@ -0,0 +1,26 @@ +package com.njcn.gather.tool.parsepqdif.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * PQDIF 文件和解析结果详情。 + */ +@Data +@ApiModel("PQDIF文件和解析结果详情") +public class CsPqdifPathDetailVO { + + @ApiModelProperty("PQDIF记录ID") + private String id; + + @ApiModelProperty("PQDIF名称") + private String name; + + @ApiModelProperty("完整PQDIF解析结果JSON") + private String jsonStr; + + @JsonIgnore + private byte[] pqdifContent; +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathVO.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathVO.java new file mode 100644 index 0000000..eb722c3 --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathVO.java @@ -0,0 +1,55 @@ +package com.njcn.gather.tool.parsepqdif.pojo.vo; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * PQDIF 存储记录列表项。 + */ +@Data +@ApiModel("PQDIF存储记录列表项") +public class CsPqdifPathVO { + + @ApiModelProperty("PQDIF记录ID") + private String id; + + @ApiModelProperty("PQDIF名称") + private String name; + + @ApiModelProperty("native解析库版本") + private String nativeVersion; + + @ApiModelProperty("Record总数") + private Long recordCount; + + @ApiModelProperty("Observation Record总数") + private Long observationCount; + + @ApiModelProperty("每个Series返回的样例采样值数量") + private Integer sampleValueCount; + + @ApiModelProperty("状态,1-正常,0-删除") + private Integer state; + + @ApiModelProperty("解析结果:1-成功,0-失败") + private Integer result; + + @ApiModelProperty("解析提示、失败原因或解析结论JSON") + private JsonNode msg; + + @ApiModelProperty("创建人") + private String createBy; + + @ApiModelProperty("创建时间") + private LocalDateTime createTime; + + @ApiModelProperty("更新人") + private String updateBy; + + @ApiModelProperty("更新时间") + private LocalDateTime updateTime; +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java index 58061d6..be1e8fd 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/reader/PqdifNativeReader.java @@ -4,15 +4,20 @@ import com.njcn.gather.tool.parsepqdif.nativebridge.PqdifNativeLibraryLoader; import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse; import com.njcn.pqdif.nativebridge.PqdifBasicNative; import com.njcn.pqdif.nativebridge.PqdifNativeSession; +import org.springframework.stereotype.Component; import java.nio.file.Path; import java.time.LocalDateTime; -import java.util.*; import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +@Component public class PqdifNativeReader { private static final String STATUS_SUCCESS = "SUCCESS"; + private static final String DATA_SUCCESS = "DATA_SUCCESS"; + private static final String DATA_FAILED = "DATA_FAILED"; private static final int DEFAULT_SAMPLE_VALUE_COUNT = 5; public PqdifParseResponse read(Path pqdifPath, String fileName) { @@ -40,10 +45,11 @@ public class PqdifNativeReader { recordVO.setRecordIndex(recordIndex); recordVO.setTypeGuid(recordInfo.typeGuid); recordVO.setTypeName(recordInfo.typeName); - recordVO.setObservation(isObservation(recordInfo)); + boolean observationRecord = isObservation(recordInfo); + recordVO.setObservation(observationRecord); response.getRecords().add(recordVO); - if (!isObservation(recordInfo)) { + if (!observationRecord) { continue; } @@ -120,7 +126,7 @@ public class PqdifNativeReader { vo.setScale(seriesInfo.scale); vo.setOffset(seriesInfo.offset); } catch (Throwable e) { - vo.setDataStatus("DATA_FAILED"); + vo.setDataStatus(DATA_FAILED); vo.setDataMessage("getSeriesInfo failed, channel=" + channelIndex + ", series=" + seriesIndex + ", error=" + e.getMessage()); @@ -132,12 +138,12 @@ public class PqdifNativeReader { try { double[] values = observation.getSeriesData(channelIndex, seriesIndex); - vo.setDataStatus("DATA_SUCCESS"); + vo.setDataStatus(DATA_SUCCESS); vo.setDataMessage(null); vo.setValueCount(values == null ? 0 : values.length); vo.setFirstValues(firstValues(values, DEFAULT_SAMPLE_VALUE_COUNT)); } catch (Throwable e) { - vo.setDataStatus("DATA_FAILED"); + vo.setDataStatus(DATA_FAILED); vo.setDataMessage("getSeriesData failed, channel=" + channelIndex + ", series=" + seriesIndex + ", seriesBaseType=" + vo.getSeriesBaseType() @@ -183,4 +189,4 @@ public class PqdifNativeReader { return dateTime.toString(); } -} \ No newline at end of file +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/CsPqdifPathService.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/CsPqdifPathService.java new file mode 100644 index 0000000..4928f76 --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/CsPqdifPathService.java @@ -0,0 +1,29 @@ +package com.njcn.gather.tool.parsepqdif.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.njcn.gather.tool.parsepqdif.pojo.param.CsPqdifPathParam; +import com.njcn.gather.tool.parsepqdif.pojo.param.PqdifParseResultSaveParam; +import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO; +import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathVO; + +import java.util.List; + +/** + * PQDIF 存储记录服务。 + */ +public interface CsPqdifPathService { + + List listPqdifPaths(CsPqdifPathParam.ListParam param); + + JsonNode getPqdifParseMsg(String pqdifId); + + CsPqdifPathDetailVO getPqdifParseDetail(String pqdifId); + + boolean addPqdifPath(CsPqdifPathParam param); + + boolean updatePqdifPath(CsPqdifPathParam.UpdateParam param); + + boolean deletePqdifPath(List ids); + + boolean savePqdifParseResult(String pqdifId, PqdifParseResultSaveParam param); +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImpl.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImpl.java new file mode 100644 index 0000000..d408388 --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImpl.java @@ -0,0 +1,185 @@ +package com.njcn.gather.tool.parsepqdif.service.impl; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.fasterxml.jackson.databind.JsonNode; +import com.njcn.gather.tool.parsepqdif.mapper.CsPqdifPathMapper; +import com.njcn.gather.tool.parsepqdif.pojo.param.CsPqdifPathParam; +import com.njcn.gather.tool.parsepqdif.pojo.param.PqdifParseResultSaveParam; +import com.njcn.gather.tool.parsepqdif.pojo.po.CsPqdifPathPO; +import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO; +import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathVO; +import com.njcn.gather.tool.parsepqdif.service.CsPqdifPathService; +import com.njcn.web.utils.RequestUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * PQDIF 存储记录服务实现。 + */ +@Service +@RequiredArgsConstructor +public class CsPqdifPathServiceImpl implements CsPqdifPathService { + + private static final int STATE_NORMAL = 1; + + private static final int STATE_DELETED = 0; + + private final CsPqdifPathMapper csPqdifPathMapper; + + @Override + public List listPqdifPaths(CsPqdifPathParam.ListParam param) { + CsPqdifPathParam.ListParam checkedParam = param == null ? new CsPqdifPathParam.ListParam() : param; + return csPqdifPathMapper.selectPqdifPathList( + trimToNull(checkedParam.getKeyword()), + checkedParam.getResult()); + } + + @Override + public JsonNode getPqdifParseMsg(String pqdifId) { + String id = requireText(pqdifId, "PQDIF记录ID不能为空"); + CsPqdifPathVO pqdifPath = csPqdifPathMapper.selectPqdifParseMsgById(id); + return pqdifPath == null ? null : pqdifPath.getMsg(); + } + + @Override + public CsPqdifPathDetailVO getPqdifParseDetail(String pqdifId) { + String id = requireText(pqdifId, "PQDIF记录ID不能为空"); + return csPqdifPathMapper.selectPqdifPathDetailById(id); + } + + @Override + @Transactional + public boolean addPqdifPath(CsPqdifPathParam param) { + CsPqdifPathParam checkedParam = requireParam(param); + LocalDateTime now = LocalDateTime.now(); + CsPqdifPathPO pqdifPath = buildPqdifPath(checkedParam); + pqdifPath.setId(UUID.randomUUID().toString().replace("-", "")); + pqdifPath.setState(STATE_NORMAL); + pqdifPath.setCreateBy(currentUserId()); + pqdifPath.setCreateTime(now); + pqdifPath.setUpdateBy(currentUserId()); + pqdifPath.setUpdateTime(now); + return csPqdifPathMapper.insert(pqdifPath) > 0; + } + + @Override + @Transactional + public boolean updatePqdifPath(CsPqdifPathParam.UpdateParam param) { + CsPqdifPathParam.UpdateParam checkedParam = requireUpdateParam(param); + requirePqdifPath(checkedParam.getId()); + CsPqdifPathPO pqdifPath = buildPqdifPath(checkedParam); + pqdifPath.setId(checkedParam.getId()); + pqdifPath.setUpdateBy(currentUserId()); + pqdifPath.setUpdateTime(LocalDateTime.now()); + return csPqdifPathMapper.updateById(pqdifPath) > 0; + } + + @Override + @Transactional + public boolean deletePqdifPath(List ids) { + if (ids == null || ids.isEmpty()) { + throw new IllegalArgumentException("PQDIF记录ID不能为空"); + } + CsPqdifPathPO pqdifPath = new CsPqdifPathPO(); + pqdifPath.setState(STATE_DELETED); + pqdifPath.setUpdateBy(currentUserId()); + pqdifPath.setUpdateTime(LocalDateTime.now()); + return csPqdifPathMapper.update(pqdifPath, new LambdaUpdateWrapper() + .in(CsPqdifPathPO::getId, ids) + .eq(CsPqdifPathPO::getState, STATE_NORMAL)) > 0; + } + + @Override + @Transactional + public boolean savePqdifParseResult(String pqdifId, PqdifParseResultSaveParam param) { + if (param == null) { + throw new IllegalArgumentException("PQDIF解析结果不能为空"); + } + CsPqdifPathPO pqdifPath = requirePqdifPath(pqdifId); + pqdifPath.setNativeVersion(trimToNull(param.getNativeVersion())); + pqdifPath.setRecordCount(param.getRecordCount()); + pqdifPath.setObservationCount(param.getObservationCount()); + pqdifPath.setSampleValueCount(param.getSampleValueCount()); + pqdifPath.setResult(normalizeResult(param.getResult())); + pqdifPath.setMsg(param.getMsg()); + pqdifPath.setJsonStr(trimToNull(param.getJsonStr())); + pqdifPath.setUpdateBy(currentUserId()); + pqdifPath.setUpdateTime(LocalDateTime.now()); + return csPqdifPathMapper.updateById(pqdifPath) > 0; + } + + private CsPqdifPathPO buildPqdifPath(CsPqdifPathParam param) { + CsPqdifPathPO pqdifPath = new CsPqdifPathPO(); + pqdifPath.setName(requireText(param.getName(), "PQDIF名称不能为空")); + pqdifPath.setPqdifContent(param.getPqdifContent()); + return pqdifPath; + } + + private CsPqdifPathParam requireParam(CsPqdifPathParam param) { + if (param == null) { + throw new IllegalArgumentException("PQDIF记录参数不能为空"); + } + return param; + } + + private CsPqdifPathParam.UpdateParam requireUpdateParam(CsPqdifPathParam.UpdateParam param) { + if (param == null) { + throw new IllegalArgumentException("PQDIF记录参数不能为空"); + } + requireText(param.getId(), "PQDIF记录ID不能为空"); + return param; + } + + private CsPqdifPathPO requirePqdifPath(String pqdifId) { + String id = requireText(pqdifId, "PQDIF记录ID不能为空"); + CsPqdifPathPO pqdifPath = csPqdifPathMapper.selectById(id); + if (pqdifPath == null || !Integer.valueOf(STATE_NORMAL).equals(pqdifPath.getState())) { + throw new IllegalArgumentException("PQDIF记录不存在或已删除"); + } + return pqdifPath; + } + + private Integer normalizeResult(Integer result) { + if (result == null) { + throw new IllegalArgumentException("解析结果不能为空"); + } + if (result != 0 && result != 1) { + throw new IllegalArgumentException("解析结果只能是0或1"); + } + return result; + } + + private String requireText(String value, String message) { + String text = trimToNull(value); + if (text == null) { + throw new IllegalArgumentException(message); + } + return text; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + return text.isEmpty() ? null : text; + } + + private boolean isBlank(String value) { + return trimToNull(value) == null; + } + + private String currentUserId() { + try { + String userId = RequestUtil.getUserId(); + return isBlank(userId) ? "未知用户" : userId; + } catch (Exception ex) { + return "未知用户"; + } + } +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java index 102aed3..02cfa9f 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/ParsePqdifServiceImpl.java @@ -3,87 +3,115 @@ package com.njcn.gather.tool.parsepqdif.service.impl; import com.njcn.gather.tool.parsepqdif.pojo.vo.PqdifParseResponse; import com.njcn.gather.tool.parsepqdif.reader.PqdifNativeReader; import com.njcn.gather.tool.parsepqdif.service.ParsePqdifService; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; -import java.nio.file.*; -import java.util.ArrayList; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Locale; @Slf4j @Service +@RequiredArgsConstructor public class ParsePqdifServiceImpl implements ParsePqdifService { private static final String STATUS_FAILED = "FAILED"; + private static final String DEFAULT_SUFFIX = ".pqd"; + private static final String PQDIF_SUFFIX = ".pqdif"; + private static final String TEMP_DIR_NAME = "cn-tool-pqdif-upload"; + private static final String EMPTY_FILE_MESSAGE = "PQDIF文件不能为空"; + private static final String UNSUPPORTED_FILE_MESSAGE = "仅支持 .pqd 或 .pqdif 格式文件"; + private static final String DEFAULT_FAILED_MESSAGE = "PQDIF解析失败"; + private static final String UNKNOWN_FAILED_REASON = "请检查文件内容或原生解析库状态"; - private final PqdifNativeReader pqdifNativeReader = new PqdifNativeReader(); + private final PqdifNativeReader pqdifNativeReader; @Override public PqdifParseResponse parse(MultipartFile pqdifFile) { if (pqdifFile == null || pqdifFile.isEmpty()) { - return failed(null, "PQDIF文件不能为空"); + return failed(null, EMPTY_FILE_MESSAGE); + } + + String originalFilename = pqdifFile.getOriginalFilename(); + String suffix = getSupportedSuffix(originalFilename); + if (suffix == null) { + return failed(originalFilename, UNSUPPORTED_FILE_MESSAGE); } Path tempFile = null; - try { - tempFile = createTempPqdifFile(pqdifFile); - return pqdifNativeReader.read(tempFile, pqdifFile.getOriginalFilename()); + tempFile = createTempPqdifFile(pqdifFile, suffix); + return pqdifNativeReader.read(tempFile, originalFilename); } catch (Exception e) { - log.error("PQDIF解析失败,fileName={}", pqdifFile.getOriginalFilename(), e); - return failed(pqdifFile.getOriginalFilename(), e.getMessage()); + log.error("PQDIF解析失败,fileName={}", originalFilename, e); + return failed(originalFilename, buildFailedMessage(e)); } finally { - if (tempFile != null) { - try { - Files.deleteIfExists(tempFile); - } catch (Exception e) { - log.warn("删除PQDIF临时文件失败,path={}", tempFile, e); - } - } + deleteTempFile(tempFile); } } - private Path createTempPqdifFile(MultipartFile pqdifFile) throws Exception { - String originalFilename = pqdifFile.getOriginalFilename(); - String suffix = getSuffix(originalFilename); - - Path uploadDir = Paths.get("D:", "CN_Tool_Runtime", "pqdif-upload"); + private Path createTempPqdifFile(MultipartFile pqdifFile, String suffix) throws Exception { + // 原生解析库只接收文件路径,因此上传内容需先落到系统临时目录。 + Path uploadDir = Paths.get(System.getProperty("java.io.tmpdir"), TEMP_DIR_NAME); Files.createDirectories(uploadDir); - String safeFileName = "parse-pqdif-" + System.currentTimeMillis() + "-" + - java.util.UUID.randomUUID().toString().replace("-", "") + suffix; - - Path tempFile = uploadDir.resolve(safeFileName); - + Path tempFile = Files.createTempFile(uploadDir, "parse-pqdif-", suffix); try (InputStream inputStream = pqdifFile.getInputStream()) { Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING); } - return tempFile; } - private String getSuffix(String originalFilename) { - if (originalFilename == null) { - return ".pqd"; + private void deleteTempFile(Path tempFile) { + if (tempFile == null) { + return; + } + try { + Files.deleteIfExists(tempFile); + } catch (Exception e) { + log.warn("删除PQDIF临时文件失败,path={}", tempFile, e); + } + } + + private String getSupportedSuffix(String originalFilename) { + if (originalFilename == null || originalFilename.trim().isEmpty()) { + return null; } int index = originalFilename.lastIndexOf('.'); if (index < 0 || index == originalFilename.length() - 1) { - return ".pqd"; + return null; } - return originalFilename.substring(index); + String suffix = originalFilename.substring(index).toLowerCase(Locale.ROOT); + if (DEFAULT_SUFFIX.equals(suffix) || PQDIF_SUFFIX.equals(suffix)) { + return suffix; + } + return null; + } + + private String resolveErrorMessage(Exception e) { + String message = e.getMessage(); + if (message == null || message.trim().isEmpty()) { + return UNKNOWN_FAILED_REASON; + } + return message; + } + + private String buildFailedMessage(Exception e) { + return DEFAULT_FAILED_MESSAGE + ":" + resolveErrorMessage(e); } private PqdifParseResponse failed(String fileName, String message) { PqdifParseResponse response = new PqdifParseResponse(); response.setStatus(STATUS_FAILED); - response.setMessage(message == null ? "PQDIF解析失败" : message); + response.setMessage(message == null ? DEFAULT_FAILED_MESSAGE : message); response.setFileName(fileName); response.setRecordCount(0L); response.setObservationCount(0L); @@ -92,4 +120,4 @@ public class ParsePqdifServiceImpl implements ParsePqdifService { response.setObservations(new ArrayList()); return response; } -} \ No newline at end of file +} diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/typehandler/JsonNodeTypeHandler.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/typehandler/JsonNodeTypeHandler.java new file mode 100644 index 0000000..93b2b03 --- /dev/null +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/typehandler/JsonNodeTypeHandler.java @@ -0,0 +1,51 @@ +package com.njcn.gather.tool.parsepqdif.typehandler; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 将数据库 JSON 文本映射为 Jackson JsonNode,便于接口直接返回结构化解析结果。 + */ +public class JsonNodeTypeHandler extends BaseTypeHandler { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, JsonNode parameter, JdbcType jdbcType) + throws SQLException { + ps.setString(i, parameter.toString()); + } + + @Override + public JsonNode getNullableResult(ResultSet rs, String columnName) throws SQLException { + return parse(rs.getString(columnName)); + } + + @Override + public JsonNode getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return parse(rs.getString(columnIndex)); + } + + @Override + public JsonNode getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return parse(cs.getString(columnIndex)); + } + + private JsonNode parse(String value) throws SQLException { + if (value == null || value.trim().isEmpty()) { + return null; + } + try { + return OBJECT_MAPPER.readTree(value); + } catch (Exception ex) { + throw new SQLException("解析JSON字段失败", ex); + } + } +} diff --git a/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java b/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java index 9a6a64d..88e8342 100644 --- a/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java +++ b/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java @@ -36,7 +36,10 @@ public class AuthGlobalFilter implements Filter, Ordered { "/admin/login", "/admin/getPublicKey", "/event/list/transient/page", - "/event/list/transient/page/debug" + "/event/list/transient/page/debug", + "/steady/checksquare/create", + "/steady/checksquare/detail", + "/steady/checksquare/item-detail" ); @Resource