feat(mms-mapping): 添加ICD一致性校验功能并重构设备类型管理

- 在MappingController中新增ICD一致性校验接口checkIcdJsonConsistency
- 添加IcdConsistencyCheckService服务实现ICD映射JSON一致性校验逻辑
- 添加IcdConsistencyCheckRequest和IcdConsistencyCheckResponse相关数据传输对象
- 在CsIcdPathPO中新增icdContent字段存储ICD内容字节数组
- 在CsIcdPathMapper中新增selectIcdPathList方法支持关键词搜索
- 移除设备类型相关的控制器、服务接口及实现类(MmsDeviceTypeController等)
- 更新.gitignore文件排除特定jar包路径
- 在pom.xml中添加device-types模块依赖和JNA库依赖
- 更新README.md文档添加device-types模块说明
- 重命名steady-DataView为steady-dataView模块名统一格式
This commit is contained in:
2026-06-15 08:38:19 +08:00
parent 1edee2bf12
commit fd6e5097d7
91 changed files with 2620 additions and 882 deletions

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>steady</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>check-square</artifactId>
<dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>steady-dataView</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -41,6 +41,7 @@ public class SteadyChecksquareInfluxQueryComponent {
private static final DateTimeFormatter INFLUX_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final int QUERY_WINDOW_DAYS = 1;
private static final ThreadLocal<Map<String, List<SteadyChecksquareValuePointBO>>> REQUEST_VALUE_CACHE =
new ThreadLocal<Map<String, List<SteadyChecksquareValuePointBO>>>();
@@ -79,8 +80,7 @@ public class SteadyChecksquareInfluxQueryComponent {
log.info("数据校验指标值 InfluxDB 查询开始measurement={}field={}lineId={}phase={}statType={}query={}",
field.getMeasurement(), field.getField(), field.getLineId(), field.getPhase(), field.getStatType(), query);
try {
String body = executeQuery(query);
List<SteadyChecksquareValuePointBO> points = parseValuePoints(body, intervalMinutes);
List<SteadyChecksquareValuePointBO> points = queryValuePointsByWindow(field, startTime, endTime, intervalMinutes);
if (cache != null) {
cache.put(cacheKey, new ArrayList<SteadyChecksquareValuePointBO>(points));
}
@@ -124,7 +124,8 @@ public class SteadyChecksquareInfluxQueryComponent {
log.info("数据校验指标值 InfluxDB 批量查询开始measurement={}fieldCount={}lineId={}phase={}statType={}query={}",
first.getMeasurement(), missingFields.size(), first.getLineId(), first.getPhase(), first.getStatType(), query);
try {
Map<String, List<SteadyChecksquareValuePointBO>> queried = parseBatchValuePoints(executeQuery(query), intervalMinutes);
Map<String, List<SteadyChecksquareValuePointBO>> queried =
queryBatchValuePointsByWindow(missingFields, startTime, endTime, intervalMinutes);
for (SteadyTrendResolvedFieldBO field : missingFields) {
List<SteadyChecksquareValuePointBO> points = queried.get(field.getField());
if (points == null) {
@@ -155,6 +156,51 @@ public class SteadyChecksquareInfluxQueryComponent {
return query + "|intervalMinutes=" + intervalMinutes;
}
private List<SteadyChecksquareValuePointBO> queryValuePointsByWindow(SteadyTrendResolvedFieldBO field,
LocalDateTime startTime,
LocalDateTime endTime,
int intervalMinutes) {
List<SteadyChecksquareValuePointBO> result = new ArrayList<SteadyChecksquareValuePointBO>();
LocalDateTime windowStart = startTime;
while (!windowStart.isAfter(endTime)) {
LocalDateTime windowEnd = min(windowStart.plusDays(QUERY_WINDOW_DAYS).minusNanos(1), endTime);
result.addAll(parseValuePoints(executeQuery(buildValuePointQuery(field, windowStart, windowEnd)), intervalMinutes));
windowStart = windowEnd.plusNanos(1);
}
return result;
}
private Map<String, List<SteadyChecksquareValuePointBO>> queryBatchValuePointsByWindow(List<SteadyTrendResolvedFieldBO> fields,
LocalDateTime startTime,
LocalDateTime endTime,
int intervalMinutes) {
Map<String, List<SteadyChecksquareValuePointBO>> result =
new LinkedHashMap<String, List<SteadyChecksquareValuePointBO>>();
for (SteadyTrendResolvedFieldBO field : fields) {
result.put(field.getField(), new ArrayList<SteadyChecksquareValuePointBO>());
}
LocalDateTime windowStart = startTime;
while (!windowStart.isAfter(endTime)) {
LocalDateTime windowEnd = min(windowStart.plusDays(QUERY_WINDOW_DAYS).minusNanos(1), endTime);
Map<String, List<SteadyChecksquareValuePointBO>> windowResult =
parseBatchValuePoints(executeQuery(buildBatchValuePointQuery(fields, windowStart, windowEnd)), intervalMinutes);
for (Map.Entry<String, List<SteadyChecksquareValuePointBO>> entry : windowResult.entrySet()) {
List<SteadyChecksquareValuePointBO> points = result.get(entry.getKey());
if (points == null) {
points = new ArrayList<SteadyChecksquareValuePointBO>();
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;
}
public String buildValuePointQuery(SteadyTrendResolvedFieldBO field, LocalDateTime startTime, LocalDateTime endTime) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT \"").append(field.getField()).append("\" AS \"value\"");

View File

@@ -9,7 +9,6 @@ import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareHistoryQueryParam;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareCreateVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO;
@@ -36,7 +35,7 @@ import java.util.List;
@Slf4j
@Api(tags = "数据校验")
@RestController
@RequestMapping("/steady/data-view/checksquare")
@RequestMapping("/steady/checksquare")
@RequiredArgsConstructor
public class SteadyChecksquareController extends BaseController {
@@ -55,10 +54,10 @@ public class SteadyChecksquareController extends BaseController {
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("新增数据校验记录")
@PostMapping("/create")
public HttpResult<SteadyChecksquareCreateVO> create(@RequestBody @Validated SteadyChecksquareQueryParam param) {
public HttpResult<SteadyChecksquareTaskVO> create(@RequestBody @Validated SteadyChecksquareQueryParam param) {
String methodDescribe = getMethodDescribe("create");
LogUtil.njcnDebug(log, "{}开始新增数据校验记录param={}", methodDescribe, param);
SteadyChecksquareCreateVO result = checksquareService.create(param);
SteadyChecksquareTaskVO result = checksquareService.create(param);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
}

View File

@@ -8,7 +8,9 @@ public final class SteadyChecksquareConst {
public static final int STATE_DELETED = 0;
public static final int STATE_ENABLED = 1;
public static final String TASK_STATUS_RUNNING = "RUNNING";
public static final String TASK_STATUS_SUCCESS = "SUCCESS";
public static final String TASK_STATUS_FAIL = "FAIL";
public static final String DETAIL_TYPE_SEGMENT = "SEGMENT";
public static final String DETAIL_TYPE_VALUE_ORDER = "VALUE_ORDER";
public static final String DETAIL_TYPE_HARMONIC_PARITY = "HARMONIC_PARITY";

View File

@@ -3,7 +3,6 @@ package com.njcn.gather.steady.checksquare.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareHistoryQueryParam;
import com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareCreateVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO;
@@ -17,7 +16,7 @@ public interface SteadyChecksquareService {
Page<SteadyChecksquareTaskVO> query(SteadyChecksquareHistoryQueryParam param);
SteadyChecksquareCreateVO create(SteadyChecksquareQueryParam param);
SteadyChecksquareTaskVO create(SteadyChecksquareQueryParam param);
boolean delete(List<String> taskIds);

View File

@@ -17,7 +17,6 @@ import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareDetailPO;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareItemPO;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareStatSummaryPO;
import com.njcn.gather.steady.checksquare.pojo.po.SteadyChecksquareTaskPO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareCreateVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareHarmonicParityRuleVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO;
@@ -52,6 +51,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
@@ -67,8 +67,7 @@ import java.util.Set;
import java.util.stream.Collectors;
/**
* 数据校验服务实现
*/
* 数据校验服务实现 */
@Slf4j
@Service
@RequiredArgsConstructor
@@ -80,6 +79,8 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
private static final int FLICKER_LONG_INTERVAL_MINUTES = 120;
private static final int HARMONIC_AGGREGATE_ORDER_START = 2;
private static final int HARMONIC_AGGREGATE_ORDER_END = 50;
private static final long MAX_CREATE_RANGE_DAYS = 7L;
private static final int DETAIL_SAVE_BATCH_SIZE = 1000;
private final SteadyTrendIndicatorCatalog indicatorCatalog;
private final SteadyChecksquareInfluxQueryComponent influxQueryComponent;
@@ -116,18 +117,25 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
}
@Override
public SteadyChecksquareCreateVO create(SteadyChecksquareQueryParam param) {
public SteadyChecksquareTaskVO create(SteadyChecksquareQueryParam param) {
validateCreateBaseParam(param);
String lineId = trimToNull(param.getLineId());
LocalDateTime startTime = parseRequiredTime(param.getTimeStart(), "开始时间不能为空");
LocalDateTime endTime = parseRequiredTime(param.getTimeEnd(), "结束时间不能为空");
SteadyChecksquareTaskPO existedTask = findExistingTask(lineId, startTime, endTime);
if (existedTask != null) {
return toTaskVO(existedTask);
}
prepareCreateContext(param);
influxQueryComponent.enableRequestCache();
SteadyChecksquareQueryVO result;
try {
result = calculate(param);
SteadyChecksquareQueryVO result = calculate(param);
SteadyChecksquareTaskPO task = saveResultInTransaction(param, result);
return toTaskVO(task);
} finally {
influxQueryComponent.clearRequestCache();
}
SteadyChecksquareTaskPO task = saveResultInTransaction(param, result);
return toCreateVO(task);
}
@Override
public boolean delete(List<String> taskIds) {
List<String> ids = normalizeTextList(taskIds);
@@ -224,7 +232,6 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
.in(SteadyChecksquareTaskPO::getId, taskIds)
.eq(SteadyChecksquareTaskPO::getState, SteadyChecksquareConst.STATE_ENABLED);
boolean taskResult = taskService.update(taskWrapper);
// 检测项同步置为删除态避免已删任务下的 item-detail 被继续访问
LambdaUpdateWrapper<SteadyChecksquareItemPO> itemWrapper = new LambdaUpdateWrapper<SteadyChecksquareItemPO>()
.set(SteadyChecksquareItemPO::getState, SteadyChecksquareConst.STATE_DELETED)
.in(SteadyChecksquareItemPO::getTaskId, taskIds)
@@ -233,6 +240,51 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
return taskResult;
}
private CreateContext prepareCreateContext(SteadyChecksquareQueryParam param) {
validateParam(param);
String lineId = trimToNull(param.getLineId());
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<String> indicatorCodes = normalizeTextList(param.getIndicatorCodes());
for (String indicatorCode : indicatorCodes) {
requireIndicator(indicatorCode);
}
validateCreateTimeRange(startTime, endTime);
return new CreateContext(lineId, linePath, startTime, endTime, intervalMinutes, indicatorCodes);
}
private void validateCreateTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
if (Duration.between(startTime, endTime).compareTo(Duration.ofDays(MAX_CREATE_RANGE_DAYS)) > 0) {
throw fail("数据校验时间范围不能超过7天");
}
}
private SteadyChecksquareTaskPO findExistingTask(String lineId, LocalDateTime startTime, LocalDateTime endTime) {
List<SteadyChecksquareTaskPO> tasks = taskService.lambdaQuery()
.eq(SteadyChecksquareTaskPO::getState, SteadyChecksquareConst.STATE_ENABLED)
.eq(SteadyChecksquareTaskPO::getLineId, lineId)
.eq(SteadyChecksquareTaskPO::getTimeStart, startTime)
.eq(SteadyChecksquareTaskPO::getTimeEnd, endTime)
.orderByDesc(SteadyChecksquareTaskPO::getCreateTime)
.list();
return tasks == null || tasks.isEmpty() ? null : tasks.get(0);
}
private void markTaskFail(String taskId, String message) {
LambdaUpdateWrapper<SteadyChecksquareTaskPO> wrapper = new LambdaUpdateWrapper<SteadyChecksquareTaskPO>()
.set(SteadyChecksquareTaskPO::getTaskStatus, SteadyChecksquareConst.TASK_STATUS_FAIL)
.set(SteadyChecksquareTaskPO::getResultMessage, limitMessage(message))
.set(SteadyChecksquareTaskPO::getUpdateTime, LocalDateTime.now())
.eq(SteadyChecksquareTaskPO::getId, taskId)
.eq(SteadyChecksquareTaskPO::getState, SteadyChecksquareConst.STATE_ENABLED);
taskService.update(wrapper);
}
private SteadyChecksquareQueryVO calculate(SteadyChecksquareQueryParam param) {
validateParam(param);
String lineId = trimToNull(param.getLineId());
@@ -275,10 +327,21 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
return transactionTemplate.execute(status -> saveResult(param, result));
}
private SteadyChecksquareTaskPO saveResultInTransaction(String taskId, SteadyChecksquareQueryParam param, SteadyChecksquareQueryVO result) {
if (transactionTemplate == null) {
return saveResult(taskId, param, result);
}
return transactionTemplate.execute(status -> saveResult(taskId, param, result));
}
private SteadyChecksquareTaskPO saveResult(SteadyChecksquareQueryParam param, SteadyChecksquareQueryVO result) {
return saveResult(null, param, result);
}
private SteadyChecksquareTaskPO saveResult(String taskId, SteadyChecksquareQueryParam param, SteadyChecksquareQueryVO result) {
LocalDateTime now = LocalDateTime.now();
SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO();
task.setId(SteadyChecksquareIdUtil.uuid());
task.setId(trimToNull(taskId) == null ? SteadyChecksquareIdUtil.uuid() : taskId);
task.setTaskNo(SteadyChecksquareIdUtil.taskNo());
task.setLineId(result.getLineId());
task.setLineName(result.getLineName());
@@ -296,7 +359,11 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
task.setState(SteadyChecksquareConst.STATE_ENABLED);
task.setCreateTime(now);
task.setUpdateTime(now);
taskService.save(task);
if (trimToNull(taskId) == null) {
taskService.save(task);
} else {
updateCompletedTask(task);
}
List<SteadyChecksquareItemPO> itemPOs = new ArrayList<SteadyChecksquareItemPO>();
List<SteadyChecksquareStatSummaryPO> summaryPOs = new ArrayList<SteadyChecksquareStatSummaryPO>();
@@ -315,11 +382,33 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
statSummaryService.saveBatch(summaryPOs);
}
if (!detailPOs.isEmpty()) {
detailService.saveBatch(detailPOs);
saveDetailBatchInChunks(detailPOs);
}
return task;
}
private void updateCompletedTask(SteadyChecksquareTaskPO task) {
LambdaUpdateWrapper<SteadyChecksquareTaskPO> wrapper = new LambdaUpdateWrapper<SteadyChecksquareTaskPO>()
.set(SteadyChecksquareTaskPO::getLineName, task.getLineName())
.set(SteadyChecksquareTaskPO::getIntervalMinutes, task.getIntervalMinutes())
.set(SteadyChecksquareTaskPO::getTaskStatus, task.getTaskStatus())
.set(SteadyChecksquareTaskPO::getItemCount, task.getItemCount())
.set(SteadyChecksquareTaskPO::getAbnormalItemCount, task.getAbnormalItemCount())
.set(SteadyChecksquareTaskPO::getMinDataIntegrity, task.getMinDataIntegrity())
.set(SteadyChecksquareTaskPO::getResultMessage, task.getResultMessage())
.set(SteadyChecksquareTaskPO::getUpdateTime, task.getUpdateTime())
.eq(SteadyChecksquareTaskPO::getId, task.getId())
.eq(SteadyChecksquareTaskPO::getState, SteadyChecksquareConst.STATE_ENABLED);
taskService.update(wrapper);
}
private void saveDetailBatchInChunks(List<SteadyChecksquareDetailPO> detailPOs) {
for (int start = 0; start < detailPOs.size(); start += DETAIL_SAVE_BATCH_SIZE) {
int end = Math.min(start + DETAIL_SAVE_BATCH_SIZE, detailPOs.size());
detailService.saveBatch(detailPOs.subList(start, end));
}
}
private SteadyChecksquareItemPO buildItemPO(String taskId, SteadyChecksquareItemVO item, LocalDateTime now) {
SteadyChecksquareItemPO po = new SteadyChecksquareItemPO();
po.setId(SteadyChecksquareIdUtil.uuid());
@@ -444,7 +533,6 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
}
}
for (Map.Entry<String, List<SteadyTrendResolvedFieldBO>> entry : fieldMap.entrySet()) {
// 预取只依赖请求级缓存后续缺数和规则校验复用同一批 Influx 结果
influxQueryComponent.queryValuePointMap(entry.getValue(), startTime, endTime, intervalMap.get(entry.getKey()));
}
}
@@ -727,13 +815,28 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
}
private void validateParam(SteadyChecksquareQueryParam param) {
validateParam(param, false);
}
private void validateCreateBaseParam(SteadyChecksquareQueryParam param) {
if (param == null) {
throw fail("数据校验参数不能为空");
}
if (trimToNull(param.getLineId()) == null) {
throw fail("监测点 ID 不能为空");
throw fail("监测点ID不能为空");
}
if (normalizeTextList(param.getIndicatorCodes()).isEmpty()) {
parseRequiredTime(param.getTimeStart(), "开始时间不能为空");
parseRequiredTime(param.getTimeEnd(), "结束时间不能为空");
}
private void validateParam(SteadyChecksquareQueryParam param, boolean allowEmptyIndicators) {
if (param == null) {
throw fail("数据校验参数不能为空");
}
if (trimToNull(param.getLineId()) == null) {
throw fail("监测点ID不能为空");
}
if (!allowEmptyIndicators && normalizeTextList(param.getIndicatorCodes()).isEmpty()) {
throw fail("指标不能为空");
}
parseRequiredTime(param.getTimeStart(), "开始时间不能为空");
@@ -800,7 +903,7 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
int start = Math.max(HARMONIC_AGGREGATE_ORDER_START, indicator.getHarmonicOrderStart());
int end = Math.min(HARMONIC_AGGREGATE_ORDER_END, indicator.getHarmonicOrderEnd());
if (start > end) {
throw fail("谐波次数只能在 " + HARMONIC_AGGREGATE_ORDER_START + "" + HARMONIC_AGGREGATE_ORDER_END + " 之间");
throw fail("谐波次数只能在" + HARMONIC_AGGREGATE_ORDER_START + "" + HARMONIC_AGGREGATE_ORDER_END + "之间");
}
List<Integer> result = new ArrayList<Integer>();
for (int order = start; order <= end; order++) {
@@ -914,20 +1017,6 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
return vo;
}
private SteadyChecksquareCreateVO toCreateVO(SteadyChecksquareTaskPO task) {
SteadyChecksquareCreateVO vo = new SteadyChecksquareCreateVO();
vo.setTaskId(task.getId());
vo.setTaskNo(task.getTaskNo());
vo.setLineId(task.getLineId());
vo.setLineName(task.getLineName());
vo.setTimeStart(formatTime(task.getTimeStart()));
vo.setTimeEnd(formatTime(task.getTimeEnd()));
vo.setIntervalMinutes(task.getIntervalMinutes());
vo.setItemCount(task.getItemCount());
vo.setAbnormalItemCount(task.getAbnormalItemCount());
return vo;
}
private SteadyChecksquareItemVO toItemVO(SteadyChecksquareItemPO item) {
SteadyChecksquareItemVO vo = new SteadyChecksquareItemVO();
vo.setItemId(item.getId());
@@ -1167,6 +1256,14 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
return builder.toString();
}
private String limitMessage(String message) {
String text = trimToNull(message);
if (text == null) {
return "数据校验任务执行失败";
}
return text.length() > 2000 ? text.substring(0, 2000) : text;
}
private List<Integer> readIntegerList(String json) {
if (trimToNull(json) == null) {
return new ArrayList<Integer>();
@@ -1179,6 +1276,18 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
}
}
private List<String> readStringList(String json) {
if (trimToNull(json) == null) {
return new ArrayList<String>();
}
try {
String[] values = objectMapper.readValue(json, String[].class);
return normalizeTextList(Arrays.asList(values));
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage());
}
}
private List<BigDecimal> readBigDecimalList(String json) {
if (trimToNull(json) == null) {
return new ArrayList<BigDecimal>();
@@ -1194,4 +1303,26 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
private BusinessException fail(String message) {
return new BusinessException(CommonResponseEnum.FAIL, message);
}
private static class CreateContext {
private final String lineId;
private final AddLedgerLinePathVO linePath;
private final LocalDateTime startTime;
private final LocalDateTime endTime;
private final int intervalMinutes;
private final List<String> indicatorCodes;
private CreateContext(String lineId, AddLedgerLinePathVO linePath, LocalDateTime startTime,
LocalDateTime endTime, int intervalMinutes, List<String> indicatorCodes) {
this.lineId = lineId;
this.linePath = linePath;
this.startTime = startTime;
this.endTime = endTime;
this.intervalMinutes = intervalMinutes;
this.indicatorCodes = indicatorCodes;
}
}
}

View File

@@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS `steady_checksquare_task` (
`interval_minutes` INT NULL COMMENT '默认统计间隔,单位分钟',
`indicator_codes_json` JSON NULL COMMENT '请求指标编码列表',
`indicator_codes_text` VARCHAR(2000) NULL COMMENT '请求指标编码检索文本,格式 |code1|code2|',
`task_status` VARCHAR(32) NOT NULL DEFAULT 'SUCCESS' COMMENT '任务状态SUCCESS/FAIL',
`task_status` VARCHAR(32) NOT NULL DEFAULT 'SUCCESS' COMMENT '任务状态:RUNNING/SUCCESS/FAIL',
`item_count` INT NOT NULL DEFAULT 0 COMMENT '检测项数量',
`abnormal_item_count` INT NOT NULL DEFAULT 0 COMMENT '异常检测项数量',
`min_data_integrity` DECIMAL(12,6) NOT NULL DEFAULT 0.000000 COMMENT '最低数据完整性',

View File

@@ -10,6 +10,7 @@ import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@@ -141,6 +142,38 @@ class SteadyChecksquareInfluxQueryComponentTest {
}
}
@Test
void shouldSplitLongValuePointQueryByDay() throws Exception {
AtomicInteger requestCount = new AtomicInteger();
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/query", exchange -> {
int index = requestCount.incrementAndGet();
byte[] body = ("{\"results\":[{\"series\":[{\"values\":["
+ "[\"2026-05-0" + index + "T00:00:00Z\"," + index + "]"
+ "]}]}]}").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);
List<com.njcn.gather.steady.checksquare.pojo.bo.SteadyChecksquareValuePointBO> result =
component.queryValuePoints(buildField("h_2"),
LocalDateTime.of(2026, 5, 1, 0, 0, 0),
LocalDateTime.of(2026, 5, 3, 0, 0, 0), 1);
Assertions.assertEquals(3, requestCount.get());
Assertions.assertEquals(3, result.size());
} finally {
server.stop(0);
}
}
private SteadyTrendResolvedFieldBO buildField(String fieldName) {
SteadyTrendResolvedFieldBO field = new SteadyTrendResolvedFieldBO();
field.setMeasurement("data_harmonic");

View File

@@ -6,8 +6,9 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.List;
/**
* 数据校验接口契约测试
@@ -17,7 +18,7 @@ class SteadyChecksquareControllerTest {
@Test
void shouldExposeChecksquareQueryEndpointInSeparateController() throws Exception {
RequestMapping requestMapping = SteadyChecksquareController.class.getAnnotation(RequestMapping.class);
Assertions.assertArrayEquals(new String[]{"/steady/data-view/checksquare"}, requestMapping.value());
Assertions.assertArrayEquals(new String[]{"/steady/checksquare"}, requestMapping.value());
Method queryMethod = SteadyChecksquareController.class.getDeclaredMethod("query", com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareHistoryQueryParam.class);
PostMapping queryMapping = queryMethod.getAnnotation(PostMapping.class);
@@ -40,4 +41,16 @@ class SteadyChecksquareControllerTest {
PostMapping deleteMapping = deleteMethod.getAnnotation(PostMapping.class);
Assertions.assertArrayEquals(new String[]{"/delete"}, deleteMapping.value());
}
@Test
void shouldKeepCreateResponseAsTaskSummaryWithoutDetailItems() throws Exception {
Method createMethod = SteadyChecksquareController.class.getDeclaredMethod("create",
com.njcn.gather.steady.checksquare.pojo.param.SteadyChecksquareQueryParam.class);
ParameterizedType resultType = (ParameterizedType) createMethod.getGenericReturnType();
Assertions.assertEquals(com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO.class,
resultType.getActualTypeArguments()[0]);
Assertions.assertThrows(NoSuchFieldException.class,
() -> com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO.class.getDeclaredField("items"));
}
}

View File

@@ -18,6 +18,7 @@ import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareItemVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareQueryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareStatSummaryVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareTaskVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderDetailVO;
import com.njcn.gather.steady.checksquare.pojo.vo.SteadyChecksquareValueOrderRuleVO;
import com.njcn.gather.steady.checksquare.service.SteadyChecksquareDetailService;
@@ -48,13 +49,13 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 数据校验服务测试
*/
* 鏁版嵁鏍獙鏈嶅姟娴嬭瘯銆? */
class SteadyChecksquareServiceImplTest {
@Test
@@ -64,6 +65,177 @@ class SteadyChecksquareServiceImplTest {
Assertions.assertNull(createMethod.getAnnotation(Transactional.class));
}
@Test
void shouldRejectCreateWhenTimeRangeExceedsSevenDays() {
AddLedgerService addLedgerService = mock(AddLedgerService.class);
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
LambdaQueryChainWrapper<SteadyChecksquareTaskPO> taskQuery = mock(LambdaQueryChainWrapper.class);
when(taskService.lambdaQuery()).thenReturn(taskQuery);
when(taskQuery.eq(any(), any())).thenReturn(taskQuery);
when(taskQuery.orderByDesc(any())).thenReturn(taskQuery);
when(taskQuery.list()).thenReturn(Collections.emptyList());
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), addLedgerService, taskService,
mock(SteadyChecksquareItemService.class), mock(SteadyChecksquareStatSummaryService.class),
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
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));
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-08 00:01:00");
Assertions.assertThrows(RuntimeException.class, () -> service.create(param));
verify(taskService, never()).save(any());
}
@Test
void shouldNotRejectCreateByIndicatorCountWithinSevenDays() {
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<SteadyChecksquareTaskPO> taskQuery = mock(LambdaQueryChainWrapper.class);
when(taskService.lambdaQuery()).thenReturn(taskQuery);
when(taskQuery.eq(any(), any())).thenReturn(taskQuery);
when(taskQuery.orderByDesc(any())).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 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<LocalDateTime>());
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<SteadyChecksquareTaskPO> taskCaptor = ArgumentCaptor.forClass(SteadyChecksquareTaskPO.class);
when(taskService.save(taskCaptor.capture())).thenReturn(true);
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Arrays.asList("V_RMS", "V_LINE_RMS", "FREQ", "I_RMS", "I_THD"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-03 23:59:00");
SteadyChecksquareTaskVO result = service.create(param);
Assertions.assertEquals(taskCaptor.getValue().getId(), result.getTaskId());
Assertions.assertEquals(Integer.valueOf(5), result.getItemCount());
verify(itemService).saveBatch(any());
}
@Test
void shouldReturnExistingTaskSummaryWhenCreateMatchesLineAndTime() {
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
LambdaQueryChainWrapper<SteadyChecksquareTaskPO> taskQuery = mock(LambdaQueryChainWrapper.class);
SteadyChecksquareTaskPO task = new SteadyChecksquareTaskPO();
task.setId("task-001");
task.setTaskNo("CS202605010001");
task.setLineId("line-001");
task.setLineName("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.setTaskStatus("SUCCESS");
task.setItemCount(1);
task.setAbnormalItemCount(0);
task.setMinDataIntegrity(BigDecimal.ONE.setScale(6));
task.setCreateTime(LocalDateTime.of(2026, 5, 1, 1, 0));
task.setState(1);
when(taskService.lambdaQuery()).thenReturn(taskQuery);
when(taskQuery.eq(any(), any())).thenReturn(taskQuery);
when(taskQuery.orderByDesc(any())).thenReturn(taskQuery);
when(taskQuery.list()).thenReturn(Collections.singletonList(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());
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
param.setIndicatorCodes(Collections.singletonList("I_RMS"));
param.setTimeStart("2026-05-01 00:00:00");
param.setTimeEnd("2026-05-01 00:01:00");
SteadyChecksquareTaskVO result = service.create(param);
Assertions.assertEquals("task-001", result.getTaskId());
Assertions.assertEquals("CS202605010001", result.getTaskNo());
Assertions.assertEquals("SUCCESS", result.getTaskStatus());
verify(taskService, never()).save(any());
}
@Test
void shouldCreateTaskSynchronouslyAndReturnTaskSummary() {
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<SteadyChecksquareTaskPO> taskQuery = mock(LambdaQueryChainWrapper.class);
when(taskService.lambdaQuery()).thenReturn(taskQuery);
when(taskQuery.eq(any(), any())).thenReturn(taskQuery);
when(taskQuery.orderByDesc(any())).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 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<LocalDateTime>());
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<SteadyChecksquareTaskPO> taskCaptor = ArgumentCaptor.forClass(SteadyChecksquareTaskPO.class);
when(taskService.save(taskCaptor.capture())).thenReturn(true);
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setLineId("line-001");
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("SUCCESS", result.getTaskStatus());
Assertions.assertEquals(Integer.valueOf(1), result.getItemCount());
verify(itemService).saveBatch(any());
verify(statSummaryService).saveBatch(any());
}
@Test
void shouldUseFixedFlickerIntervalsPerIndicator() {
SteadyChecksquareInfluxQueryComponent influxQueryComponent = mock(SteadyChecksquareInfluxQueryComponent.class);
@@ -81,7 +253,7 @@ class SteadyChecksquareServiceImplTest {
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("进线一");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -125,7 +297,7 @@ class SteadyChecksquareServiceImplTest {
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("进线一");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -164,7 +336,7 @@ class SteadyChecksquareServiceImplTest {
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("进线一");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -218,7 +390,7 @@ class SteadyChecksquareServiceImplTest {
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("进线一");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -267,7 +439,7 @@ class SteadyChecksquareServiceImplTest {
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("进线一");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -314,7 +486,7 @@ class SteadyChecksquareServiceImplTest {
.thenReturn(emptyRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("进线一");
linePath.setLineName("杩涚嚎涓€");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -376,7 +548,7 @@ class SteadyChecksquareServiceImplTest {
task.setId("task-001");
task.setState(1);
task.setLineId("line-001");
task.setLineName("进线一");
task.setLineName("杩涚嚎涓€");
task.setTimeStart(LocalDateTime.of(2026, 5, 1, 0, 0));
task.setTimeEnd(LocalDateTime.of(2026, 5, 1, 0, 1));
task.setIntervalMinutes(1);
@@ -481,14 +653,14 @@ class SteadyChecksquareServiceImplTest {
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
result.setLineId("line-001");
result.setLineName("进线一");
result.setLineName("杩涚嚎涓€");
result.setTimeStart("2026-05-01 00:00:00");
result.setTimeEnd("2026-05-01 00:01:00");
result.setIntervalMinutes(1);
SteadyChecksquareItemVO item = new SteadyChecksquareItemVO();
item.setItemKey("line-001|V_RMS");
item.setIndicatorCode("V_RMS");
item.setIndicatorName("相电压有效值");
item.setIndicatorName("鐩哥數鍘嬫湁鏁堝€?);
item.setIntervalMinutes(1);
item.setHasData(true);
item.setExpectedPointCount(2);
@@ -519,6 +691,41 @@ class SteadyChecksquareServiceImplTest {
verify(statSummaryService).saveBatch(any());
}
@Test
void shouldSaveDetailResultsInChunks() {
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
SteadyChecksquareItemService itemService = mock(SteadyChecksquareItemService.class);
SteadyChecksquareStatSummaryService statSummaryService = mock(SteadyChecksquareStatSummaryService.class);
SteadyChecksquareDetailService detailService = mock(SteadyChecksquareDetailService.class);
SteadyChecksquareServiceImpl service = new SteadyChecksquareServiceImpl(new SteadyTrendIndicatorCatalog(),
mock(SteadyChecksquareInfluxQueryComponent.class), new SteadyChecksquareCalculator(),
mock(SteadyChecksquareValueOrderRuleComponent.class), mock(SteadyChecksquareHarmonicParityRuleComponent.class),
new AddDataTimeSlotCalculator(), mock(AddLedgerService.class), taskService,
itemService, statSummaryService, detailService, new ObjectMapper());
SteadyChecksquareQueryParam param = new SteadyChecksquareQueryParam();
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
result.setLineId("line-001");
result.setLineName("line-001");
result.setTimeStart("2026-05-01 00:00:00");
result.setTimeEnd("2026-05-01 00:01:00");
result.setIntervalMinutes(1);
SteadyChecksquareItemVO item = buildOrderItem(true, BigDecimal.ONE.setScale(6));
item.setItemKey("line-001|V_RMS");
item.setIndicatorCode("V_RMS");
for (int i = 0; i < 1001; i++) {
SteadyChecksquareValueOrderDetailVO detail = new SteadyChecksquareValueOrderDetailVO();
detail.setTime("2026-05-01 00:00:00");
detail.setPhase("A");
item.getAbnormalDetails().add(detail);
}
result.getItems().add(item);
saveResult(service, param, result);
verify(detailService, times(2)).saveBatch(any());
}
@Test
void shouldCountNoDataItemAsAbnormalWhenSavingTask() {
SteadyChecksquareTaskService taskService = mock(SteadyChecksquareTaskService.class);
@@ -532,7 +739,7 @@ class SteadyChecksquareServiceImplTest {
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
result.setLineId("line-001");
result.setLineName("进线一");
result.setLineName("杩涚嚎涓€");
result.setTimeStart("2026-05-01 00:00:00");
result.setTimeEnd("2026-05-01 00:01:00");
result.setIntervalMinutes(1);
@@ -712,3 +919,5 @@ class SteadyChecksquareServiceImplTest {
return new SteadyChecksquareHarmonicParityRuleVO();
}
}

View File

@@ -13,7 +13,8 @@
<packaging>pom</packaging>
<modules>
<module>steady-DataView</module>
<module>steady-dataView</module>
<module>check-square</module>
</modules>
<properties>

View File

@@ -9,7 +9,7 @@
<version>1.0.0</version>
</parent>
<artifactId>steady-DataView</artifactId>
<artifactId>steady-dataView</artifactId>
<dependencies>
<dependency>

View File

@@ -1,44 +0,0 @@
package com.njcn.gather.steady.checksquare.pojo.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 新增数据校验结果。
*/
@Data
@ApiModel("新增数据校验结果")
public class SteadyChecksquareCreateVO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("任务 ID")
private String taskId;
@ApiModelProperty("任务编号")
private String taskNo;
@ApiModelProperty("监测点 ID")
private String lineId;
@ApiModelProperty("监测点名称")
private String lineName;
@ApiModelProperty("开始时间")
private String timeStart;
@ApiModelProperty("结束时间")
private String timeEnd;
@ApiModelProperty("统计间隔,单位分钟")
private Integer intervalMinutes;
@ApiModelProperty("检测项数量")
private Integer itemCount;
@ApiModelProperty("异常检测项数量")
private Integer abnormalItemCount;
}

View File

@@ -1,522 +0,0 @@
# 数据校验 API 调试文档
## 1. 基础信息
- 模块:`steady/steady-DataView`
- 控制器:`com.njcn.gather.steady.checksquare.controller.SteadyChecksquareController`
- 接口前缀:`/steady/data-view/checksquare`
- 本地默认地址:`http://localhost:18192`
- Content-Type`application/json`
- 认证:除登录和 Swagger 资源外,请求需要携带登录后的 `Authorization` 请求头。
- 数据库结果表:`steady_checksquare_task``steady_checksquare_item``steady_checksquare_stat_summary``steady_checksquare_detail`
通用请求头:
```http
Authorization: Bearer <token>
Content-Type: application/json
```
通用返回结构:
```json
{
"code": 200,
"message": "success",
"data": {}
}
```
> 实际 `code`、`message` 字段以项目公共 `HttpResult` 封装为准,下面示例重点展示 `data` 内容。
## 2. 查询数据校验历史记录
- 方法:`POST`
- 路径:`/steady/data-view/checksquare/query`
- 返回:`HttpResult<Page<SteadyChecksquareTaskVO>>`
- 说明:分页查询已落库的数据校验任务,按创建时间倒序返回。
请求示例:
```json
{
"pageNum": 1,
"pageSize": 10,
"lineId": "line-001",
"indicatorCode": "V_RMS",
"timeStart": "2026-05-01 00:00:00",
"timeEnd": "2026-05-01 23:59:59",
"hasAbnormal": true
}
```
请求字段:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `pageNum` | 否 | 当前页码,继承自 `BaseParam` |
| `pageSize` | 否 | 每页数量,继承自 `BaseParam` |
| `lineId` | 否 | 监测点 ID精确匹配 |
| `indicatorCode` | 否 | 指标编码,按任务指标集合匹配 |
| `timeStart` | 否 | 检测开始时间下限,格式 `yyyy-MM-dd HH:mm:ss` |
| `timeEnd` | 否 | 检测结束时间上限,格式 `yyyy-MM-dd HH:mm:ss` |
| `hasAbnormal` | 否 | 是否只查询存在异常项的任务;`true` 表示 `abnormalItemCount > 0` |
返回示例:
```json
{
"records": [
{
"taskId": "8f7a4d6d1f3145a88b6f9d7a8e6c1001",
"taskNo": "CS202606101630001",
"lineId": "line-001",
"lineName": "进线一",
"timeStart": "2026-05-01 00:00:00",
"timeEnd": "2026-05-01 23:59:59",
"intervalMinutes": 1,
"taskStatus": "SUCCESS",
"itemCount": 2,
"abnormalItemCount": 1,
"minDataIntegrity": 0.999306,
"createTime": "2026-06-10 16:30:00"
}
],
"total": 1,
"size": 10,
"current": 1,
"pages": 1
}
```
cURL 示例:
```bash
curl -X POST "http://localhost:18192/steady/data-view/checksquare/query" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"pageNum":1,"pageSize":10,"lineId":"line-001","indicatorCode":"V_RMS","hasAbnormal":true}'
```
## 3. 新增数据校验记录
- 方法:`POST`
- 路径:`/steady/data-view/checksquare/create`
- 返回:`HttpResult<SteadyChecksquareCreateVO>`
- 说明:按监测点、指标和时间范围实时查询 InfluxDB执行缺数校验、指标值大小关系校验、谐波奇偶关系校验并将任务、检测项、统计摘要和明细写入 MySQL。
请求示例:
```json
{
"lineId": "line-001",
"indicatorCodes": ["V_RMS", "FREQ", "V_HARMONIC"],
"timeStart": "2026-05-01 00:00:00",
"timeEnd": "2026-05-01 23:59:59"
}
```
请求字段:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `lineId` | 是 | 监测点 ID需要能在台账中找到且处于可用状态 |
| `indicatorCodes` | 是 | 指标编码列表,来自 `/steady/data-view/indicator-tree` |
| `timeStart` | 是 | 检测开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
| `timeEnd` | 是 | 检测结束时间,格式 `yyyy-MM-dd HH:mm:ss` |
返回示例:
```json
{
"taskId": "8f7a4d6d1f3145a88b6f9d7a8e6c1001",
"taskNo": "CS202606101630001",
"lineId": "line-001",
"lineName": "进线一",
"timeStart": "2026-05-01 00:00:00",
"timeEnd": "2026-05-01 23:59:59",
"intervalMinutes": 1,
"itemCount": 3,
"abnormalItemCount": 1
}
```
cURL 示例:
```bash
curl -X POST "http://localhost:18192/steady/data-view/checksquare/create" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"lineId":"line-001","indicatorCodes":["V_RMS","FREQ","V_HARMONIC"],"timeStart":"2026-05-01 00:00:00","timeEnd":"2026-05-01 23:59:59"}'
```
调试建议:
- 新增接口会实际写入 MySQL重复调用会生成新的任务记录。
- 时间范围越大、指标越多InfluxDB 查询和明细落库耗时越高。
- 谐波类指标固定按 2-50 次聚合检测,不需要在请求体传 `harmonicOrders`
## 4. 查询数据校验任务详情
- 方法:`GET`
- 路径:`/steady/data-view/checksquare/detail`
- 返回:`HttpResult<SteadyChecksquareQueryVO>`
- 说明:按任务 ID 查询任务基础信息、检测项列表和各检测项统计摘要。检测项的明细列表通过 `/item-detail` 按需查询。
请求参数:
| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `taskId` | 是 | 数据校验任务 ID`/query``/create` 返回的 `taskId` |
请求示例:
```http
GET /steady/data-view/checksquare/detail?taskId=8f7a4d6d1f3145a88b6f9d7a8e6c1001
```
返回示例:
```json
{
"taskId": "8f7a4d6d1f3145a88b6f9d7a8e6c1001",
"taskNo": "CS202606101630001",
"lineId": "line-001",
"lineName": "进线一",
"timeStart": "2026-05-01 00:00:00",
"timeEnd": "2026-05-01 23:59:59",
"intervalMinutes": 1,
"items": [
{
"itemId": "0f4b6d6c3e9d400a902a2df101d10001",
"itemKey": "line-001|V_RMS",
"indicatorCode": "V_RMS",
"indicatorName": "相电压有效值",
"harmonicOrder": null,
"intervalMinutes": 1,
"hasData": true,
"expectedPointCount": 5760,
"actualPointCount": 5756,
"missingPointCount": 4,
"dataIntegrity": 0.999306,
"dataIntegrityText": "99.93%",
"abnormal": true,
"abnormalPointCount": 2,
"harmonicParityAbnormal": false,
"harmonicParityAbnormalPointCount": 0,
"statSummaries": [
{
"statType": "AVG",
"supported": true,
"hasData": true,
"expectedPointCount": 1440,
"actualPointCount": 1439,
"missingPointCount": 1,
"dataIntegrity": 0.999306,
"dataIntegrityText": "99.93%",
}
],
"statDetails": []
}
]
}
```
cURL 示例:
```bash
curl -X GET "http://localhost:18192/steady/data-view/checksquare/detail?taskId=8f7a4d6d1f3145a88b6f9d7a8e6c1001" \
-H "Authorization: Bearer <token>"
```
## 5. 查询检测项明细
- 方法:`GET`
- 路径:`/steady/data-view/checksquare/item-detail`
- 返回:`HttpResult<SteadyChecksquareItemDetailVO>`
- 说明:按检测项 ID、明细类型查询缺数连续区间、指标值大小关系异常点或谐波奇偶关系异常点。`pageNum``pageSize` 未同时传入时保持全量返回;两者同时传入且均大于 0 时返回当前页明细,并在结果中带回分页元数据。
请求参数:
| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `itemId` | 是 | 检测项 ID即任务详情中每个 `items[].itemId` |
| `detailType` | 是 | 明细类型:`SEGMENT``VALUE_ORDER``HARMONIC_PARITY` |
| `statType` | 否 | 统计类型:`AVG``MAX``MIN``CP95`;查询缺数区间时建议传入 |
| `pageNum` | 否 | 明细分页页码;与 `pageSize` 同时传入且大于 0 时启用分页 |
| `pageSize` | 否 | 明细分页条数;与 `pageNum` 同时传入且大于 0 时启用分页 |
### 5.1 查询缺数连续区间
请求示例:
```http
GET /steady/data-view/checksquare/item-detail?itemId=0f4b6d6c3e9d400a902a2df101d10001&detailType=SEGMENT&statType=AVG
```
返回示例:
```json
{
"itemId": "0f4b6d6c3e9d400a902a2df101d10001",
"detailType": "SEGMENT",
"statType": "AVG",
"pageNum": null,
"pageSize": null,
"total": null,
"segments": [
{
"startTime": "2026-05-01 00:00:00",
"endTime": "2026-05-01 00:09:00",
"status": "NORMAL",
"harmonicOrder": null,
"missingPointCount": 0,
"durationMinutes": 10
},
{
"startTime": "2026-05-01 00:10:00",
"endTime": "2026-05-01 00:10:00",
"status": "MISSING",
"harmonicOrder": null,
"missingPointCount": 1,
"durationMinutes": 1
}
],
"valueOrderDetails": [],
"harmonicParityDetails": []
}
```
### 5.2 查询指标值大小关系异常明细
请求示例:
```http
GET /steady/data-view/checksquare/item-detail?itemId=0f4b6d6c3e9d400a902a2df101d10001&detailType=VALUE_ORDER&pageNum=1&pageSize=20
```
返回示例:
```json
{
"itemId": "0f4b6d6c3e9d400a902a2df101d10001",
"detailType": "VALUE_ORDER",
"statType": null,
"pageNum": 1,
"pageSize": 20,
"total": 1,
"segments": [],
"valueOrderDetails": [
{
"time": "2026-05-01 00:10:00",
"phase": "A",
"harmonicOrder": null,
"maxValue": 219.8,
"minValue": 218.1,
"avgValue": 219.0,
"cp95Value": 219.8
}
],
"harmonicParityDetails": []
}
```
### 5.3 查询谐波奇偶关系异常明细
请求示例:
```http
GET /steady/data-view/checksquare/item-detail?itemId=0f4b6d6c3e9d400a902a2df101d10002&detailType=HARMONIC_PARITY&pageNum=1&pageSize=20
```
返回示例:
```json
{
"itemId": "0f4b6d6c3e9d400a902a2df101d10002",
"detailType": "HARMONIC_PARITY",
"statType": null,
"pageNum": 1,
"pageSize": 20,
"total": 1,
"segments": [],
"valueOrderDetails": [],
"harmonicParityDetails": [
{
"time": "2026-05-01 00:10:00",
"phase": "A",
"statType": "AVG",
"evenHarmonicOrder": 4,
"evenValue": 0.32,
"oddHarmonicOrders": [3, 5],
"oddValues": [0.08, 0.09],
"oddMedianValue": 0.085,
"thresholdMultiplier": 3.0
}
]
}
```
cURL 示例:
```bash
curl -X GET "http://localhost:18192/steady/data-view/checksquare/item-detail?itemId=0f4b6d6c3e9d400a902a2df101d10001&detailType=VALUE_ORDER" \
-H "Authorization: Bearer <token>"
```
分页 cURL 示例:
```bash
curl -X GET "http://localhost:18192/steady/data-view/checksquare/item-detail?itemId=0f4b6d6c3e9d400a902a2df101d10001&detailType=VALUE_ORDER&pageNum=1&pageSize=20" \
-H "Authorization: Bearer <token>"
```
## 6. 删除数据校验任务
- 方法:`POST`
- 路径:`/steady/data-view/checksquare/delete`
- 返回:`HttpResult<Boolean>`
- 说明:按任务 ID 批量逻辑删除数据校验任务,并同步将任务下检测项置为删除态。删除后历史查询不再返回该任务,详情和检测项明细会按不存在处理。
请求示例:
```json
[
"8f7a4d6d1f3145a88b6f9d7a8e6c1001"
]
```
请求字段:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| 请求体 | 是 | 数据校验任务 ID 数组 |
返回示例:
```json
true
```
cURL 示例:
```bash
curl -X POST "http://localhost:18192/steady/data-view/checksquare/delete" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '["8f7a4d6d1f3145a88b6f9d7a8e6c1001"]'
```
## 7. 校验规则说明
### 7.1 缺数校验
- 按检测项支持的统计类型分别查询实际存在的时间点。
- 返回期望点数、实际点数、缺失点数、数据完整性和最大连续缺失时长。
- `SEGMENT` 明细按连续区间返回,`status` 可取 `NORMAL``MISSING`
- `FLUC``PST` 固定按 10 分钟间隔校验,`PLT` 固定按 120 分钟间隔校验,其余指标按监测点统计间隔校验。
### 7.2 指标值大小关系校验
- 同一时间点、同一指标、同一相别需要满足 `MAX >= CP95 >= AVG >= MIN`
- 缺少 `MAX``CP95``AVG``MIN` 任一值的时间点不计入大小关系异常,由缺数校验体现。
- 异常点写入 `VALUE_ORDER` 明细,前端可通过 `item-detail` 拉取后弹窗展示。
### 7.3 谐波奇偶关系校验
- 谐波指标固定聚合 2-50 次检测。
- 偶次谐波值需要大于 `0.1` 才继续参与奇偶关系检测。
- 异常点写入 `HARMONIC_PARITY` 明细,包含偶次谐波值、参与比较的奇次谐波值、中位数和阈值倍数。
## 8. 字段说明
### 8.1 任务字段
| 字段 | 说明 |
| --- | --- |
| `taskId` | 数据校验任务 ID |
| `taskNo` | 数据校验任务编号 |
| `lineId` | 监测点 ID |
| `lineName` | 监测点名称 |
| `timeStart` | 检测开始时间 |
| `timeEnd` | 检测结束时间 |
| `intervalMinutes` | 监测点统计间隔,单位分钟 |
| `taskStatus` | 任务状态,当前成功任务为 `SUCCESS` |
| `itemCount` | 检测项数量 |
| `abnormalItemCount` | 存在异常的检测项数量 |
| `minDataIntegrity` | 检测项中的最低数据完整性 |
| `createTime` | 任务创建时间 |
### 8.2 检测项字段
| 字段 | 说明 |
| --- | --- |
| `itemId` | 检测项 ID |
| `itemKey` | 检测项唯一键 |
| `indicatorCode` | 指标编码 |
| `indicatorName` | 指标名称 |
| `harmonicOrder` | 谐波次数;聚合检测项为空 |
| `hasData` | 当前检测项是否存在任意数据 |
| `expectedPointCount` | 期望点数 |
| `actualPointCount` | 实际点数 |
| `missingPointCount` | 缺失点数 |
| `dataIntegrity` | 数据完整性数值 |
| `dataIntegrityText` | 数据完整性文本 |
| `abnormal` | 指标值大小关系是否异常 |
| `abnormalPointCount` | 指标值大小关系异常点数 |
| `harmonicParityAbnormal` | 谐波奇偶关系是否异常 |
| `harmonicParityAbnormalPointCount` | 谐波奇偶关系异常点数 |
| `statSummaries` | 各统计类型缺数摘要 |
| `statDetails` | 兼容字段;明细建议通过 `/item-detail` 按需查询 |
### 8.3 检测项明细字段
| 字段 | 说明 |
| --- | --- |
| `itemId` | 检测项 ID |
| `detailType` | 明细类型:`SEGMENT``VALUE_ORDER``HARMONIC_PARITY` |
| `statType` | 统计类型;未传入或当前明细类型不按统计类型过滤时为空 |
| `pageNum` | 当前页码;未启用分页时为空 |
| `pageSize` | 每页条数;未启用分页时为空 |
| `total` | 当前查询条件下的总明细数;未启用分页时为空 |
| `segments` | 缺数连续区间,仅 `SEGMENT` 查询返回数据 |
| `valueOrderDetails` | 指标值大小关系异常点,仅 `VALUE_ORDER` 查询返回数据 |
| `harmonicParityDetails` | 谐波奇偶关系异常点,仅 `HARMONIC_PARITY` 查询返回数据 |
## 9. 常见错误场景
| 场景 | 后端提示 |
| --- | --- |
| 新增时 `lineId` 为空 | `监测点 ID 不能为空` |
| 新增时 `indicatorCodes` 为空 | `指标不能为空` |
| 新增时开始时间为空 | `开始时间不能为空` |
| 新增时结束时间为空 | `结束时间不能为空` |
| 开始时间大于结束时间 | `开始时间不能大于结束时间` |
| 时间格式不正确 | `时间格式不正确,仅支持 yyyy-MM-dd HH:mm:ss` |
| 监测点不存在或不可用 | `监测点不存在或不可用` |
| 指标编码不支持 | `稳态指标不支持xxx` |
| 任务不存在或已删除 | `数据校验任务不存在` |
| 检测项不存在或已删除 | `数据校验检测项不存在` |
| 删除时任务 ID 为空 | `数据校验任务 ID 不能为空` |
| 删除时任务不存在或已删除 | `数据校验任务不存在或已删除` |
| 明细类型为空 | `明细类型不能为空` |
| 明细类型不支持 | `明细类型不支持xxx` |
## 10. 调试流程建议
1. 调用 `/steady/data-view/ledger-tree` 获取可用监测点,取叶子节点 `lineId`
2. 调用 `/steady/data-view/indicator-tree` 获取可用指标编码。
3. 调用 `/steady/data-view/checksquare/create` 新增校验任务。
4. 使用返回的 `taskId` 调用 `/steady/data-view/checksquare/detail` 查看任务详情和检测项列表。
5. 使用 `items[].itemId` 调用 `/steady/data-view/checksquare/item-detail` 查看缺数区间或异常点明细。
6. 调用 `/steady/data-view/checksquare/query` 验证任务已落库,并按监测点、指标、时间和异常状态筛选历史记录。
7. 调用 `/steady/data-view/checksquare/delete` 删除任务后,再调用 `/query``/detail` 验证任务已不可见。
## 11. 环境依赖
- MySQL需要已初始化 4 张 `steady_checksquare_*` 结果表。
- InfluxDB新增校验任务时需要可访问 `steady.influxdb` 配置的时序库。
- 台账:新增校验任务时需要 `lineId` 能在台账中解析到监测点路径和统计间隔。
- 指标目录:`indicatorCodes` 必须来自后端趋势指标目录。