Browse Source

汇聚代码第一次提交

feature/agg
yinhuaiwei 1 month ago
parent
commit
d518479bb4
132 changed files with 9182 additions and 39 deletions
  1. +2
    -2
      inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/AlgCategoryEnum.java
  2. +5
    -4
      inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/DefectLevel.java
  3. +2
    -2
      inspect-ivs/src/main/java/com/inspect/ivs/controller/IvsStreamController.java
  4. +57
    -0
      inspect-main/inspect-main-agg/pom.xml
  5. +20
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/AggregationApplication.java
  6. +28
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggCalendarEvent.java
  7. +180
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggTaskCalendarGenerator.java
  8. +340
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggTaskCalendarService.java
  9. +83
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/AggProperties.java
  10. +44
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/RestTemplateConfig.java
  11. +19
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/SyncDefaults.java
  12. +38
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/dto/AggMonitorPatrolPointVo.java
  13. +15
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/dto/VideoRequest.java
  14. +13
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggAlgSubtype.java
  15. +26
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggAnalysisDefectMonth.java
  16. +43
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataArea.java
  17. +42
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataDevice.java
  18. +37
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataDevicetype.java
  19. +40
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataEqpbook.java
  20. +41
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataEqpbookChannel.java
  21. +44
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataPatrolpoint.java
  22. +64
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggDefectRecord.java
  23. +50
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggInspectionReport.java
  24. +65
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggPatrolTask.java
  25. +49
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggPatrolTaskStatus.java
  26. +45
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggResultAnalysis.java
  27. +31
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggStation.java
  28. +27
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggSyncLog.java
  29. +37
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/enums/AlgCategory.java
  30. +25
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggAnalysisDefectMonthMapper.java
  31. +30
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataAreaMapper.java
  32. +26
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataDeviceMapper.java
  33. +26
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataDevicetypeMapper.java
  34. +28
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataEqpbookChannelMapper.java
  35. +26
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataEqpbookMapper.java
  36. +31
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataPatrolpointMapper.java
  37. +25
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggDefectRecordMapper.java
  38. +26
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggInspectionReportMapper.java
  39. +31
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggPatrolTaskMapper.java
  40. +46
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggPatrolTaskStatusMapper.java
  41. +22
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggResultAnalysisMapper.java
  42. +24
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggStationMapper.java
  43. +14
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggSyncLogMapper.java
  44. +12
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/BasedataAlgSubtypeMapper.java
  45. +61
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/BaseSyncScheduler.java
  46. +28
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/DefectRecordSyncScheduler.java
  47. +53
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/DynamicSyncScheduler.java
  48. +31
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/RealtimeViewerCleanupScheduler.java
  49. +33
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/ResultAnalysisSyncScheduler.java
  50. +100
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggAnalysisDefectMonthSyncService.java
  51. +87
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggDeviceTreeService.java
  52. +236
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggStatisticService.java
  53. +99
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AreaSyncService.java
  54. +100
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/ChannelSyncService.java
  55. +185
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DefectRecordSyncService.java
  56. +103
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DeviceSyncService.java
  57. +102
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DevicetypeSyncService.java
  58. +103
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/EqpbookSyncService.java
  59. +217
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/EqpbookTreeService.java
  60. +104
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/InspectionReportSyncService.java
  61. +120
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/PatrolTaskSyncService.java
  62. +99
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/PointSyncService.java
  63. +130
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/ResultAnalysisSyncService.java
  64. +47
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/StationRegistry.java
  65. +147
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/TaskExecSyncService.java
  66. +386
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/station/StationApiClient.java
  67. +23
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/station/TokenData.java
  68. +103
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/support/Rows.java
  69. +42
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/support/SyncLogWriter.java
  70. +16
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/PlaybackSession.java
  71. +55
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/PlaybackSessionRegistry.java
  72. +33
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamBinding.java
  73. +65
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamRegistry.java
  74. +340
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamService.java
  75. +105
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeViewerRegistry.java
  76. +18
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/StationStream.java
  77. +35
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/StationStreamRegistry.java
  78. +170
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/ZlmApiClient.java
  79. +251
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggAreaController.java
  80. +65
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggCalendarController.java
  81. +55
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggDefectRecordController.java
  82. +42
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggDeviceTreeController.java
  83. +53
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggInspectionReportController.java
  84. +81
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolPointQueryController.java
  85. +47
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolTaskController.java
  86. +50
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolTaskStatusController.java
  87. +34
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggStationController.java
  88. +69
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggStatisticController.java
  89. +56
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggTreeController.java
  90. +313
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggVideoController.java
  91. +146
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/StationProxyController.java
  92. +358
    -0
      inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/VideoProxyController.java
  93. +34
    -0
      inspect-main/inspect-main-agg/src/main/resources/application.yml
  94. +429
    -0
      inspect-main/inspect-main-agg/src/main/resources/db/schema.sql
  95. +54
    -0
      inspect-main/inspect-main-agg/src/main/resources/mapper/AggAnalysisDefectMonthMapper.xml
  96. +81
    -0
      inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataAreaMapper.xml
  97. +82
    -0
      inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataDeviceMapper.xml
  98. +76
    -0
      inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataDevicetypeMapper.xml
  99. +87
    -0
      inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataEqpbookChannelMapper.xml
  100. +81
    -0
      inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataEqpbookMapper.xml

inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/AlgTypeEnum.java → inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/AlgCategoryEnum.java View File


+ 5
- 4
inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/DefectLevel.java View File

@ -4,11 +4,12 @@ import lombok.Getter;
@Getter
public enum DefectLevel {
// 0=无, 1=紧急, 2=严重, 3=一般
// 0=无, 1=紧急, 2=严重, 3=一般, 4=轻微
NONE(0, "无"),
URGENT(1, ""),
SERIOUS(2, ""),
NORMAL(3, "");
URGENT(1, "重大"),
SERIOUS(2, "严重"),
NORMAL(3, "一般"),
MINOR(4, "轻微");
private final Integer code;
private final String info;


+ 2
- 2
inspect-ivs/src/main/java/com/inspect/ivs/controller/IvsStreamController.java View File

@ -63,7 +63,7 @@ public class IvsStreamController {
// .rtspURL("rtsp://admin:wd19216811@192.168.1.249:554/h264/ch39/main/av_stream")
// .build();
ivsVideoRtspView = new IvsVideoRtspView();
ivsVideoRtspView.setRtspURL("rtsp://admin:htjc2018@192.168.1.134:554/cam/realmonitor?channel=3&subtype=0");
ivsVideoRtspView.setRtspURL("rtsp://admin:htjc2018@192.168.1.64:554/Streaming/Channels/102");
} else {
String rtspUrl = (String) this.redisService.redisTemplate.opsForValue().get("sys_config:RTSP_" + ivsStreamStartVo.getCameraCode());
if (StringUtils.isNotEmpty(rtspUrl)) {
@ -116,7 +116,7 @@ public class IvsStreamController {
// .rtspURL("rtsp://admin:wd19216811@192.168.1.249:554/h264/ch39/main/av_stream")
// .build();
ivsVideoRtspView = new IvsVideoRtspView();
ivsVideoRtspView.setRtspURL("rtsp://admin:htjc2018@192.168.1.134:554/cam/realmonitor?channel=3&subtype=0");
ivsVideoRtspView.setRtspURL("rtsp://admin:htjc2018@192.168.1.64:554/Streaming/Channels/102");
} else {
String label = java.util.UUID.randomUUID().toString().trim().replaceAll(StringUtils.DASH, StringUtils.EMPTY);
ivsVideoRtspView = ivsCommonService.postJson(


+ 57
- 0
inspect-main/inspect-main-agg/pom.xml View File

@ -0,0 +1,57 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.inspect</groupId>
<artifactId>inspect-main</artifactId>
<version>3.4.0</version>
</parent>
<artifactId>inspect-main-agg</artifactId>
<version>3.4.0</version>
<name>inspect-main-agg</name>
<description>inspect-main-agg</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.inspect</groupId>
<artifactId>inspect-base-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

+ 20
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/AggregationApplication.java View File

@ -0,0 +1,20 @@
package com.inspect.agg;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 智能巡检数据汇聚系统 - 中心采集服务(区域试点)。
*/
@SpringBootApplication
@EnableScheduling
@MapperScan("com.inspect.agg.mapper")
public class AggregationApplication {
public static void main(String[] args) {
SpringApplication.run(AggregationApplication.class, args);
}
}

+ 28
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggCalendarEvent.java View File

@ -0,0 +1,28 @@
package com.inspect.agg.calendar;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AggCalendarEvent implements Serializable {
private static final long serialVersionUID = 1L;
private String stationCode;
/** 仅用于展示:所属站点名称,不落业务表 */
private String stationName;
private String id;
private String name;
private String status;
private String taskCode;
private String source;
private LocalDateTime startTime;
private LocalDateTime endTime;
}

+ 180
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggTaskCalendarGenerator.java View File

@ -0,0 +1,180 @@
package com.inspect.agg.calendar;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.inspect.agg.entity.AggPatrolTask;
import com.inspect.agg.mapper.AggPatrolTaskMapper;
import com.inspect.base.core.enums.ExecStatus;
import com.inspect.base.core.enums.IntervalType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 根据汇聚方案规则生成指定月份的待执行计划。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AggTaskCalendarGenerator {
private static final String STATE_PENDING = "5";
private final AggPatrolTaskMapper taskMapper;
public List<AggCalendarEvent> generateMonth(String stationCode, int year, int month) {
YearMonth yearMonth = YearMonth.of(year, month);
LocalDate firstDay = yearMonth.atDay(1);
LocalDate lastDay = yearMonth.atEndOfMonth();
LocalDateTime monthStart = firstDay.atStartOfDay();
LocalDateTime monthEnd = lastDay.atTime(23, 59, 59);
LocalDateTime now = LocalDateTime.now();
if (monthEnd.isBefore(now)) {
return Collections.emptyList();
}
List<AggCalendarEvent> events = new ArrayList<>();
List<AggPatrolTask> tasks = taskMapper.selectEnabledTasks(stationCode);
for (AggPatrolTask task : tasks) {
if (!StringUtils.hasText(task.getDevType())) {
continue;
}
try {
if (ExecStatus.REGULAR.getCode().equals(task.getExecutionStatus())) {
generateRegular(task, monthStart, monthEnd, now, events);
} else if (ExecStatus.PERIODIC.getCode().equals(task.getExecutionStatus())) {
generatePeriodic(task, monthStart, monthEnd, now, events);
} else if (ExecStatus.INTERVAL.getCode().equals(task.getExecutionStatus())) {
generateInterval(task, monthStart, monthEnd, now, events);
}
} catch (Exception e) {
log.warn("generate calendar failed: station={}, taskId={}, error={}",
task.getStationCode(), task.getTaskId(), e.getMessage());
}
}
events.sort((a, b) -> a.getStartTime().compareTo(b.getStartTime()));
return events;
}
private void generateRegular(AggPatrolTask task, LocalDateTime monthStart, LocalDateTime monthEnd,
LocalDateTime now, List<AggCalendarEvent> events) {
LocalDateTime startTime = task.getFixedStartTime();
if (startTime != null && startTime.isAfter(now)
&& !startTime.isBefore(monthStart) && !startTime.isAfter(monthEnd)) {
events.add(event(task, startTime, null));
}
}
private void generatePeriodic(AggPatrolTask task, LocalDateTime monthStart, LocalDateTime monthEnd,
LocalDateTime now, List<AggCalendarEvent> events) {
if (task.getCycleStartTime() == null || task.getCycleEndTime() == null
|| !StringUtils.hasText(task.getCycleExecuteTime())) {
return;
}
LocalDateTime start = max(monthStart, task.getCycleStartTime());
LocalDateTime end = min(monthEnd, task.getCycleEndTime());
if (end.isBefore(start)) {
return;
}
String[] executeTimes = task.getCycleExecuteTime().split(",");
if (StringUtils.hasText(task.getCycleWeek())) {
String[] weeks = task.getCycleWeek().split(",");
for (LocalDate date = start.toLocalDate(); !date.isAfter(end.toLocalDate()); date = date.plusDays(1)) {
for (int i = 0; i < weeks.length && i < executeTimes.length; i++) {
if (date.getDayOfWeek() == parseDayOfWeek(weeks[i])) {
addIfVisible(task, date, executeTimes[i], now, end, events);
}
}
}
} else if (StringUtils.hasText(task.getCycleMonth())) {
String[] monthDays = task.getCycleMonth().split(",");
for (LocalDate date = start.toLocalDate(); !date.isAfter(end.toLocalDate()); date = date.plusDays(1)) {
for (int i = 0; i < monthDays.length && i < executeTimes.length; i++) {
if (date.getDayOfMonth() == Integer.parseInt(monthDays[i].trim())) {
addIfVisible(task, date, executeTimes[i], now, end, events);
}
}
}
}
}
private void generateInterval(AggPatrolTask task, LocalDateTime monthStart, LocalDateTime monthEnd,
LocalDateTime now, List<AggCalendarEvent> events) {
if (!IntervalType.BY_HOUR.getCode().equals(task.getIntervalType())
|| task.getIntervalNumber() == null || task.getIntervalNumber() <= 0
|| task.getIntervalStartTime() == null || task.getIntervalEndTime() == null
|| !StringUtils.hasText(task.getIntervalExecuteTime())) {
return;
}
LocalDateTime start = task.getIntervalStartTime();
LocalDateTime end = min(monthEnd, task.getIntervalEndTime());
LocalTime executeTime = LocalTime.parse(task.getIntervalExecuteTime().trim());
LocalDateTime cursor = LocalDateTime.of(start.toLocalDate(), executeTime);
while (cursor.isBefore(start)) {
cursor = cursor.plusHours(task.getIntervalNumber());
}
while (!cursor.isAfter(end)) {
if (cursor.isAfter(now) && !cursor.isBefore(monthStart)) {
events.add(event(task, cursor, null));
}
cursor = cursor.plusHours(task.getIntervalNumber());
}
}
private void addIfVisible(AggPatrolTask task, LocalDate date, String time,
LocalDateTime now, LocalDateTime end,
List<AggCalendarEvent> events) {
if (!StringUtils.hasText(time)) {
return;
}
LocalDateTime startTime = LocalDateTime.of(date, LocalTime.parse(time.trim()));
if (startTime.isAfter(now) && !startTime.isAfter(end)) {
events.add(event(task, startTime, null));
}
}
private AggCalendarEvent event(AggPatrolTask task, LocalDateTime startTime, LocalDateTime endTime) {
return AggCalendarEvent.builder()
.stationCode(task.getStationCode())
.stationName(task.getStationName())
.id(task.getTaskCode())
.taskCode(task.getTaskCode())
.name(task.getTaskName())
.status("pending")
.source("plan")
.startTime(startTime)
.endTime(endTime)
.build();
}
private static LocalDateTime max(LocalDateTime a, LocalDateTime b) {
return a.isAfter(b) ? a : b;
}
private static LocalDateTime min(LocalDateTime a, LocalDateTime b) {
return a.isBefore(b) ? a : b;
}
private static DayOfWeek parseDayOfWeek(String value) {
String week = value == null ? "" : value.trim();
if ("星期一".equals(week) || "1".equals(week)) return DayOfWeek.MONDAY;
if ("星期二".equals(week) || "2".equals(week)) return DayOfWeek.TUESDAY;
if ("星期三".equals(week) || "3".equals(week)) return DayOfWeek.WEDNESDAY;
if ("星期四".equals(week) || "4".equals(week)) return DayOfWeek.THURSDAY;
if ("星期五".equals(week) || "5".equals(week)) return DayOfWeek.FRIDAY;
if ("星期六".equals(week) || "6".equals(week)) return DayOfWeek.SATURDAY;
if ("星期日".equals(week) || "7".equals(week)) return DayOfWeek.SUNDAY;
throw new IllegalArgumentException("不支持的星期: " + week);
}
}

+ 340
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggTaskCalendarService.java View File

@ -0,0 +1,340 @@
package com.inspect.agg.calendar;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.inspect.agg.entity.AggPatrolTaskStatus;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggPatrolTaskStatusMapper;
import com.inspect.agg.service.StationRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 汇聚计划服务:规则生成未来计划,合并实际任务状态,并使用 Redis 缓存。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AggTaskCalendarService {
private static final String CACHE_PREFIX = "agg:calendar:v2:";
private static final Duration CACHE_TTL = Duration.ofHours(12);
private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final TypeReference<List<AggCalendarEvent>> EVENT_LIST_TYPE =
new TypeReference<List<AggCalendarEvent>>() { };
private static final TypeReference<List<Map<String, Object>>> STATS_LIST_TYPE =
new TypeReference<List<Map<String, Object>>>() { };
private final AggTaskCalendarGenerator calendarGenerator;
private final AggPatrolTaskStatusMapper taskStatusMapper;
private final StationRegistry stationRegistry;
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
public List<Map<String, Object>> year(String stationCode, int year) {
String scope = scope(stationCode);
String cacheKey = CACHE_PREFIX + "summary:" + scope + ":" + year;
List<Map<String, Object>> cached = readCache(cacheKey, STATS_LIST_TYPE);
if (cached != null) {
return cached;
}
List<Map<String, Object>> result = new ArrayList<>();
for (int month = 1; month <= 12; month++) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("month", month);
item.put("executed", 0L);
item.put("executing", 0L);
item.put("pending", 0L);
item.put("total", 0L);
result.add(item);
}
for (int month = 1; month <= 12; month++) {
List<AggCalendarEvent> events = visibleEvents(loadMonthEvents(stationCode, year, month));
Map<String, Object> item = result.get(month - 1);
long executed = count(events, "executed");
long executing = count(events, "executing");
long pending = count(events, "pending");
item.put("executed", executed);
item.put("executing", executing);
item.put("pending", pending);
item.put("total", executed + executing + pending);
}
writeCache(cacheKey, result, summaryTtl(year));
return result;
}
public List<Map<String, Object>> month(String stationCode, int year, int month) {
List<AggCalendarEvent> events = visibleEvents(loadMonthEvents(stationCode, year, month));
applyStationNames(events);
return toMaps(events);
}
public List<Map<String, Object>> day(String stationCode, int year, int month, int day) {
LocalDate target = LocalDate.of(year, month, day);
List<AggCalendarEvent> result = new ArrayList<>();
for (AggCalendarEvent event : visibleEvents(loadMonthEvents(stationCode, year, month))) {
if (event.getStartTime() != null && target.equals(event.getStartTime().toLocalDate())) {
result.add(event);
}
}
applyStationNames(result);
return toMaps(result);
}
public void evictStation(String stationCode) {
if (!StringUtils.hasText(stationCode)) {
return;
}
YearMonth now = YearMonth.now();
for (int i = -12; i <= 12; i++) {
YearMonth month = now.plusMonths(i);
deleteQuietly(monthKey(stationCode, month.getYear(), month.getMonthValue()));
}
deleteQuietly(CACHE_PREFIX + "summary:" + stationCode + ":" + now.getYear());
deleteQuietly(CACHE_PREFIX + "summary:" + stationCode + ":" + (now.getYear() + 1));
evictAllScopes();
}
@Scheduled(cron = "0 50 0 * * ?")
public void refreshFutureCalendar() {
YearMonth current = YearMonth.now();
for (AggStation station : stationRegistry.listEnabled()) {
for (int i = 0; i < 6; i++) {
YearMonth month = current.plusMonths(i);
deleteQuietly(monthKey(station.getStationCode(), month.getYear(), month.getMonthValue()));
loadStationMonthCached(station.getStationCode(), month.getYear(), month.getMonthValue());
}
}
evictAllScopes();
}
private List<AggCalendarEvent> loadMonthEvents(String stationCode, int year, int month) {
String scope = scope(stationCode);
String key = monthKey(stationCode, year, month);
List<AggCalendarEvent> cached = readCache(key, EVENT_LIST_TYPE);
if (cached != null) {
return cached;
}
List<AggCalendarEvent> result = new ArrayList<>();
if (StringUtils.hasText(stationCode)) {
result.addAll(loadStationMonth(stationCode, year, month));
} else {
for (AggStation station : stationRegistry.listEnabled()) {
result.addAll(loadStationMonthCached(station.getStationCode(), year, month));
}
result = merge(result);
}
writeCache(key, result, monthTtl(year, month));
return result;
}
private List<AggCalendarEvent> loadStationMonthCached(String stationCode, int year, int month) {
String key = monthKey(stationCode, year, month);
List<AggCalendarEvent> cached = readCache(key, EVENT_LIST_TYPE);
if (cached != null) {
return cached;
}
List<AggCalendarEvent> result = loadStationMonth(stationCode, year, month);
writeCache(key, result, monthTtl(year, month));
return result;
}
private List<AggCalendarEvent> loadStationMonth(String stationCode, int year, int month) {
List<AggCalendarEvent> events = new ArrayList<>();
events.addAll(calendarGenerator.generateMonth(stationCode, year, month));
List<AggPatrolTaskStatus> actualTasks =
taskStatusMapper.selectDailyStatsByYearAndMonth(stationCode, year, month);
for (AggPatrolTaskStatus task : actualTasks) {
if (task.getStartTime() == null) {
continue;
}
events.add(AggCalendarEvent.builder()
.stationCode(task.getStationCode())
.stationName(task.getStationName())
.id(task.getTaskPatrolledId())
.taskCode(task.getTaskCode())
.name(task.getTaskName())
.status(stateName(task.getTaskState()))
.source("actual")
.startTime(task.getStartTime())
.endTime(task.getEndTime())
.build());
}
return merge(events);
}
private static List<AggCalendarEvent> merge(List<AggCalendarEvent> events) {
Map<String, AggCalendarEvent> unique = new HashMap<>();
for (AggCalendarEvent event : events) {
if (event.getStartTime() == null) {
continue;
}
String key = event.getStationCode() + "|" + event.getTaskCode() + "|" + event.getStartTime();
AggCalendarEvent existing = unique.get(key);
if (existing == null || "actual".equals(event.getSource())) {
unique.put(key, event);
}
}
List<AggCalendarEvent> result = new ArrayList<>(unique.values());
result.sort((a, b) -> a.getStartTime().compareTo(b.getStartTime()));
return result;
}
private List<AggCalendarEvent> visibleEvents(List<AggCalendarEvent> events) {
LocalDateTime now = LocalDateTime.now();
List<AggCalendarEvent> result = new ArrayList<>();
for (AggCalendarEvent event : events) {
if ("plan".equals(event.getSource())
&& event.getStartTime() != null && !event.getStartTime().isAfter(now)) {
continue;
}
result.add(event);
}
return result;
}
private List<Map<String, Object>> toMaps(List<AggCalendarEvent> events) {
List<Map<String, Object>> result = new ArrayList<>();
for (AggCalendarEvent event : events) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("id", event.getId());
item.put("name", event.getName());
item.put("status", event.getStatus());
item.put("start", event.getStartTime() == null ? null : event.getStartTime().format(DATE_FORMATTER));
item.put("end", event.getEndTime() == null ? null : event.getEndTime().format(DATE_FORMATTER));
item.put("startTime", event.getStartTime() == null ? null : event.getStartTime().format(DATE_TIME_FORMATTER));
item.put("endTime", event.getEndTime() == null ? null : event.getEndTime().format(DATE_TIME_FORMATTER));
item.put("stationCode", event.getStationCode());
item.put("stationName", event.getStationName() != null ? event.getStationName() : event.getStationCode());
result.add(item);
}
return result;
}
private void applyStationNames(List<AggCalendarEvent> events) {
if (events == null || events.isEmpty()) {
return;
}
boolean needLookup = false;
for (AggCalendarEvent event : events) {
if (StringUtils.hasText(event.getStationCode()) && !StringUtils.hasText(event.getStationName())) {
needLookup = true;
break;
}
}
if (!needLookup) {
return;
}
Map<String, String> stationNames = new HashMap<>();
for (AggStation station : stationRegistry.listAll()) {
stationNames.put(station.getStationCode(), station.getStationName());
}
for (AggCalendarEvent event : events) {
if (StringUtils.hasText(event.getStationCode()) && !StringUtils.hasText(event.getStationName())) {
event.setStationName(stationNames.getOrDefault(event.getStationCode(), event.getStationCode()));
}
}
}
private static long count(List<AggCalendarEvent> events, String status) {
long count = 0;
for (AggCalendarEvent event : events) {
if (status.equals(event.getStatus())) {
count++;
}
}
return count;
}
private static String stateName(String state) {
if ("1".equals(state)) return "executed";
if ("2".equals(state)) return "executing";
if ("3".equals(state)) return "paused";
if ("4".equals(state)) return "terminate";
if ("5".equals(state)) return "pending";
if ("6".equals(state)) return "exceeded";
return null;
}
private String monthKey(String stationCode, int year, int month) {
return CACHE_PREFIX + "events:" + scope(stationCode) + ":"
+ year + "-" + String.format("%02d", month);
}
private static String scope(String stationCode) {
return StringUtils.hasText(stationCode) ? stationCode : "ALL";
}
private <T> T readCache(String key, TypeReference<T> type) {
try {
String value = redisTemplate.opsForValue().get(key);
return StringUtils.hasText(value) ? objectMapper.readValue(value, type) : null;
} catch (Exception e) {
log.warn("read calendar cache failed: key={}, error={}", key, e.getMessage());
return null;
}
}
private void writeCache(String key, Object value, Duration ttl) {
try {
redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), ttl);
} catch (Exception e) {
log.warn("write calendar cache failed: key={}, error={}", key, e.getMessage());
}
}
private static Duration monthTtl(int year, int month) {
YearMonth current = YearMonth.now();
YearMonth target = YearMonth.of(year, month);
if (target.equals(current)) return Duration.ofMinutes(1);
if (target.equals(current.plusMonths(1))) return Duration.ofHours(1);
return CACHE_TTL;
}
private static Duration summaryTtl(int year) {
return year == YearMonth.now().getYear() ? Duration.ofMinutes(5) : CACHE_TTL;
}
private void deleteQuietly(String key) {
try {
redisTemplate.delete(key);
} catch (Exception e) {
log.warn("delete calendar cache failed: key={}, error={}", key, e.getMessage());
}
}
private void evictAllScopes() {
YearMonth current = YearMonth.now();
for (int i = 0; i < 6; i++) {
YearMonth month = current.plusMonths(i);
deleteQuietly(monthKey(null, month.getYear(), month.getMonthValue()));
}
deleteQuietly(CACHE_PREFIX + "summary:ALL:" + current.getYear());
deleteQuietly(CACHE_PREFIX + "summary:ALL:" + (current.getYear() + 1));
}
}

+ 83
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/AggProperties.java View File

@ -0,0 +1,83 @@
package com.inspect.agg.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 汇聚采集服务配置(prefix = agg):默认值写在代码中;application.yml 已无 agg.* 配置项。
* 站点配置已迁移到 DB 表 agg_station(见 StationRegistry),不再放 yaml。
*/
@Data
@Component
@ConfigurationProperties(prefix = "agg")
public class AggProperties {
private Sync sync = new Sync();
private Proxy proxy = new Proxy();
private Video video = new Video();
private Http http = new Http();
@Data
public static class Sync {
/** 基础数据同步公共配置(轮询周期见 SyncDefaults,已写死在代码) */
private Base base = new Base();
private Dynamic dynamic = new Dynamic();
private AnalysisSummary analysisSummary = new AnalysisSummary();
@Data
public static class Base {
private int pageSize = 100;
/** 启动时先同步一次(联调用) */
private boolean runOnStart = false;
}
@Data
public static class Dynamic {
/** 启动时先同步一次 */
private boolean runOnStart = false;
}
@Data
public static class AnalysisSummary {
private boolean runOnStart = true;
}
}
@Data
public static class Proxy {
/** 允许代理的路径前缀(为空=不限制);建议按需收紧,如 patrol/、v1/ */
private java.util.List<String> allowedPrefixes = new java.util.ArrayList<>();
}
@Data
public static class Video {
/** 集控 ZLMediaKit API/播放地址(如 http://192.168.1.116:33380) */
private String zlmBaseUrl = "http://192.168.1.116:33380";
/** ZLM config.ini 的 api.secret */
private String zlmSecret = "Xg@123456";
/** 拉流目标 app */
private String app = "live";
/** 播放地址对办公网络暴露的地址(默认同 zlmBaseUrl) */
private String publicBaseUrl = "";
/** 是否启用 RTSP TCP 传输(0=TCP, 1=UDP) */
private int rtspTransport = 0;
/** 拉流重试次数(-1=无限) */
private int retryCount = -1;
/** ZLM hook 接收接口 token(内部鉴权) */
private String hookToken = "inspect-agg-hook";
/** 无 HLS 请求多少毫秒后视为空闲 */
private long hookIdleTimeoutMs = 30_000L;
}
@Data
public static class Http {
private int connectTimeoutMs = 10_000;
private int readTimeoutMs = 30_000;
}
}

+ 44
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/RestTemplateConfig.java View File

@ -0,0 +1,44 @@
package com.inspect.agg.config;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
/**
* HTTP 客户端配置:RestTemplate + 超时 + UTF-8(站点接口返回中文,必须 UTF-8 解码)。
*/
@Configuration
public class RestTemplateConfig {
private final AggProperties aggProperties;
/** 显式构造器注入(不用 Lombok,避免 IDE 静态检查误报找不到 Bean) */
public RestTemplateConfig(AggProperties aggProperties) {
this.aggProperties = aggProperties;
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
RestTemplate restTemplate = builder
.setConnectTimeout(Duration.ofMillis(aggProperties.getHttp().getConnectTimeoutMs()))
.setReadTimeout(Duration.ofMillis(aggProperties.getHttp().getReadTimeoutMs()))
.build();
// 默认 StringHttpMessageConverter 用 ISO-8859-1,会破坏站点返回的中文
List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof StringHttpMessageConverter) {
StringHttpMessageConverter stringConverter = (StringHttpMessageConverter) converter;
stringConverter.setDefaultCharset(StandardCharsets.UTF_8);
}
}
return restTemplate;
}
}

+ 19
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/SyncDefaults.java View File

@ -0,0 +1,19 @@
package com.inspect.agg.config;
/**
* 同步相关默认配置(写死在代码内;如需调整改这里并重新构建)。
*/
public final class SyncDefaults {
/** 基础数据全量轮询周期(毫秒):5 分钟 */
public static final String BASE_INTERVAL_MS = "300000";
/** 任务执行活跃子集轮询周期(毫秒):60 秒 */
public static final String DYNAMIC_INTERVAL_MS = "60000";
/** 动态数据全量对账周期(毫秒):5 分钟 */
public static final String DYNAMIC_FULL_INTERVAL_MS = "300000";
private SyncDefaults() {
}
}

+ 38
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/dto/AggMonitorPatrolPointVo.java View File

@ -0,0 +1,38 @@
package com.inspect.agg.dto;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* Response item for /agg/patrolpoint/list/monitor.
* The patrolpoint* JSON names match the station-side API contract.
*/
@Data
public class AggMonitorPatrolPointVo {
private Long id;
private String stationCode;
private String stationName;
private Long patrolpointId;
private String patrolpointCode;
private String patrolpointName;
private Long mainDeviceId;
private String deviceName;
private Long deviceId;
private Long areaId;
private String areaName;
private String patrolContent;
private String purpose;
private String patrolTypeId;
private String pointImptLevel;
private String alarmThreshold;
private String algSubtypeIds;
private String algSubtypeName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
private LocalDateTime syncedAt;
private Integer isDeleted;
}

+ 15
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/dto/VideoRequest.java View File

@ -0,0 +1,15 @@
package com.inspect.agg.dto;
import lombok.Data;
/**
* 汇聚视频接口请求体。
*/
@Data
public class VideoRequest {
private String cameraCode;
private Integer streamType = 2;
private Integer direct = 0;
private String rtspUrl;
}

+ 13
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggAlgSubtype.java View File

@ -0,0 +1,13 @@
package com.inspect.agg.entity;
import lombok.Data;
/**
* Shared station algorithm subtype dictionary (read-only).
*/
@Data
public class AggAlgSubtype {
private Long algSubtypeId;
private String algSubtypeCode;
private String algSubtypeName;
}

+ 26
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggAnalysisDefectMonth.java View File

@ -0,0 +1,26 @@
package com.inspect.agg.entity;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 站端缺陷月度汇总表。
*/
@Data
public class AggAnalysisDefectMonth {
private Long id;
private String stationCode;
/** 仅用于展示:所属站点名称,不落业务表 */
private String stationName;
private LocalDate statMonth;
private String pointType;
private Integer minorCount;
private Integer generalCount;
private Integer seriousCount;
private Integer urgentCount;
private Integer normalCount;
private LocalDateTime syncedAt;
}

+ 43
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataArea.java View File

@ -0,0 +1,43 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 区域镜像表 agg_basedata_area(源表:站点 basedata_area)。
*/
@Data
public class AggBasedataArea {
private Long id;
/** 站点编码 */
private String stationCode;
/** 仅用于展示:所属站点名称,不落业务表 */
private String stationName;
/** 源区域 id */
private Long areaId;
/** 父区域 id */
private Long parentId;
/** 祖级列表,如 ,1,2002 */
private String ancestors;
private String areaName;
private String areaCode;
private String areaAddr;
/** station / station_in / bayunit */
private String areaType;
private String deviceTypeCode;
private Integer orderNum;
/** 0正常 1停用 */
private String status;
private String remark;
/** 站点侧 update_time */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
/** 中心落库时间 */
private LocalDateTime syncedAt;
/** 0存在 1站点侧已删除 */
private Integer isDeleted;
}

+ 42
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataDevice.java View File

@ -0,0 +1,42 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 设备镜像表 agg_basedata_device(源表:站点 basedata_device,接口 /basedata/prodevmnt/device/list)。
*/
@Data
public class AggBasedataDevice {
private Long id;
private String stationCode;
/** 仅用于展示:所属站点名称,不落业务表 */
private String stationName;
/** 源设备 id */
private Long deviceId;
/** 父设备 id */
private Long parentId;
private String ancestors;
private Long areaId;
private String areaName;
private String deviceCode;
private String deviceName;
private String deviceTypeName;
private String deviceModel;
private String manufacturer;
/** 电力主设备标识 Yes/No */
private String powerMainEquFlag;
private String stationNo;
private String monitorIndexNo;
/** 0正常 1停用 */
private String status;
private Integer orderNum;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
private LocalDateTime syncedAt;
private Integer isDeleted;
}

+ 37
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataDevicetype.java View File

@ -0,0 +1,37 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 设备类型镜像表 agg_basedata_devicetype(源表:站点 basedata_devicetype,接口 /basedata/devicetype/list)。
*/
@Data
public class AggBasedataDevicetype {
private Long id;
private String stationCode;
/** 仅用于展示:所属站点名称,不落业务表 */
private String stationName;
/** 源设备类型 id */
private Long deviceTypeId;
private Long parentId;
private Integer orderNum;
private String ancestors;
private String ancestorsAddr;
private String deviceTypeName;
private String deviceTypeCode;
private String powerMainEquFlag;
private String phaseNum;
private String phase;
private String status;
private String professionType;
private Long level;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
private LocalDateTime syncedAt;
private Integer isDeleted;
}

+ 40
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataEqpbook.java View File

@ -0,0 +1,40 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 设备台账镜像表 agg_basedata_eqpbook(源表:站点 basedata_eqpbook,接口 /basedata/eqpbook/list)。
* 注意:源响应含 user/password 等敏感字段,本表不镜像。
*/
@Data
public class AggBasedataEqpbook {
private Long id;
private String stationCode;
/** 源台账 id */
private Long eqpbookId;
private String patrolDeviceCode;
private String patrolDeviceName;
private String stationName;
private Long areaId;
private String areaName;
private String deviceSource;
private String deviceModel;
private String manufacturer;
/** 设备类型(type) */
private String deviceType;
private String ipAddr;
private String port;
private String videoNvrCode;
private String mainSystemCode;
private String useMode;
private String videoMode;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
private LocalDateTime syncedAt;
private Integer isDeleted;
}

+ 41
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataEqpbookChannel.java View File

@ -0,0 +1,41 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 设备通道镜像表 agg_basedata_eqpbook_channel(源表:站点 basedata_eqpbook_channel,接口 /basedata/eqpbook/channel/list)。
*/
@Data
public class AggBasedataEqpbookChannel {
private Long id;
private String stationCode;
/** 仅用于展示:所属站点名称,不落业务表 */
private String stationName;
/** 源通道 id */
private Long channelId;
private String channelCode;
private String channelName;
/** vl/ir/ptz/ng */
private String channelType;
/** 1在线 0离线 */
private String onlineState;
private String channelContent;
private String host;
private String port;
private Long areaId;
private String areaName;
/** 所属台账 id */
private Long eqpbookId;
private String patrolDeviceCode;
private String upSystemDeviceCode;
private String deviceSource;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
private LocalDateTime syncedAt;
private Integer isDeleted;
}

+ 44
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataPatrolpoint.java View File

@ -0,0 +1,44 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 巡检点位镜像表 agg_basedata_patrolpoint(源表:站点 basedata_patrolpoint,接口 /basedata/patrolpointmnt/patrolpoint/list)。
*/
@Data
public class AggBasedataPatrolpoint {
private Long id;
private String stationCode;
/** 仅用于展示:所属站点名称,不落业务表 */
private String stationName;
/** 源点位 id */
private Long patrolPointId;
private String patrolPointCode;
private String patrolPointName;
/** 主设备 id */
private Long mainDeviceId;
private String deviceName;
/** 设备 id(真正配置点位的设备/部件) */
private Long deviceId;
private Long areaId;
private String areaName;
private String patrolContent;
private String purpose;
/** 巡视类型,逗号分隔 */
private String patrolTypeId;
/** 重要等级 Ⅰ/Ⅱ/Ⅲ */
private String pointImptLevel;
/** 告警阈值 JSON */
private String alarmThreshold;
/** Source algorithm subtype IDs, comma-separated. */
private String algSubtypeIds;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
private LocalDateTime syncedAt;
private Integer isDeleted;
}

+ 64
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggDefectRecord.java View File

@ -0,0 +1,64 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
/**
* Snapshot row returned by station-side /patrol/analysis/defect/list.
*/
@Data
public class AggDefectRecord {
@JsonIgnore
private Long id;
private String stationCode;
private String stationName;
private String deviceName;
private String channelName;
private String channelCode;
private String deviceSource;
private String pointName;
private String pointId;
private String warnStatus;
private String algName;
private String resValue;
private String img;
private String filterDesc;
private String brightDesc;
private String imgAnalyse;
private String brightImgAnalyse;
private String desc;
private String lineId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime patrolTime;
private String pointStatus;
private String imgType;
private String dataType;
private String filter;
private String requestId;
private String resStatus;
private String brightResStatus;
private String areaName;
private String areaId;
private String algType;
private String objectId;
private String imageNormalUrlPath;
private String filePath;
private String suggestion;
private String originalValue;
private String taskPatrolId;
private String taskName;
private Integer defectLevel;
@JsonIgnore
private LocalDateTime syncedAt;
@JsonIgnore
private Integer isDeleted;
}

+ 50
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggInspectionReport.java View File

@ -0,0 +1,50 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 巡视报告镜像表 agg_inspection_report。
*/
@Data
public class AggInspectionReport {
private Long id;
private String stationCode;
private Long lineId;
private String stationName;
private String voltLevel;
private String stationType;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDateTime inspectionDate;
private String inspectionTaskName;
private String envInfo;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime inspectionStartTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime inspectionEndTime;
private String patrolStatistics;
private String checkPerson;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime checkTime;
private String description;
private String filePath;
private String taskId;
private String taskResultId;
private String taskPatrolledId;
private String location;
private String filter;
private LocalDateTime syncedAt;
private Integer isDeleted;
}

+ 65
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggPatrolTask.java View File

@ -0,0 +1,65 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 任务方案镜像表 agg_patrol_task(源:patrol_task,接口 /patrol/task/list)。
*/
@Data
public class AggPatrolTask {
private Long id;
private String stationCode;
/** 仅用于展示:所属站点名称,不落业务表 */
private String stationName;
/** 源任务 id */
private Long taskId;
private String taskCode;
private String taskName;
/** 巡视类型 */
private String type;
private String priority;
private String deviceLevel;
private String deviceList;
private LocalDateTime fixedStartTime;
/** 0可用 1不可用 */
private String isEnable;
/** 状态 */
private String fileStatus;
/** 执行状态 */
private String executionStatus;
private String taskCycle;
private String cycleMonth;
private String cycleWeek;
private String cycleExecuteTime;
private LocalDateTime cycleStartTime;
private LocalDateTime cycleEndTime;
private Integer intervalNumber;
private String intervalType;
private String intervalExecuteTime;
private LocalDateTime intervalStartTime;
private LocalDateTime intervalEndTime;
private LocalDateTime invalidStartTime;
private LocalDateTime invalidEndTime;
/** 1机器人 0无人机 2视频 */
private String devType;
private String areaId;
private String areaName;
/** 字典 point_alg_category 的 dict_code,多个值逗号分隔 */
private String pointType;
/** 方案点位总数,来源:站端 /patrol/task/list 统计返回 */
private Integer totalPoints;
private String executionMode;
private String drafter;
private String specifiedAlg;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
private LocalDateTime syncedAt;
private Integer isDeleted;
}

+ 49
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggPatrolTaskStatus.java View File

@ -0,0 +1,49 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 任务执行状态镜像表 agg_patrol_task_status(源:patrol_task_status,接口 /patrol/taskstatus/list)。
*/
@Data
public class AggPatrolTaskStatus {
private Long id;
private String stationCode;
/** 仅用于展示:所属站点名称,不落业务表 */
private String stationName;
private Long lineId;
private String taskPatrolledId;
private String taskName;
private String taskCode;
/** 1已执行 2执行中 3暂停 4终止 5未执行 6超期 */
private String taskState;
private LocalDateTime planStartTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime startTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime endTime;
private String taskProgress;
private String taskEstimatedTime;
private String patrolType;
private String posType;
private String code;
private String pos;
private String execType;
private String taskType;
private String mainId;
/** ??????????? /patrol/taskstatus/list ???? */
private Integer qxNum;
/** ?????0??? 1????????? /patrol/taskstatus/list ???? */
private String fileStatus;
private String summary;
private String description;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
private LocalDateTime syncedAt;
private Integer isDeleted;
}

+ 45
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggResultAnalysis.java View File

@ -0,0 +1,45 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* result_analysis 汇聚镜像表。
*/
@Data
public class AggResultAnalysis {
private Long id;
private String stationCode;
/** 仅用于展示:所属站点名称,不落业务表 */
private String stationName;
private Long lineId;
private String resultType;
private String businessId;
private String resultContent;
private String resStatus;
private String pointStatus;
private String suggestion;
private String objectId;
private String description;
private String requestId;
private String taskPatrolId;
private String algType;
private String resValue;
private String resCode;
private String resConf;
private String resDesc;
private String result;
private String resImgUrl;
private String imageNormalUrlPath;
private String resPos;
private String filter;
private String originalValue;
private Integer defectLevel;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
private LocalDateTime syncedAt;
private Integer isDeleted;
}

+ 31
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggStation.java View File

@ -0,0 +1,31 @@
package com.inspect.agg.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
/**
* 站点配置表 agg_station(DB 维护,替代 yaml 站点列表)。
* 密码临时明文存储。
*/
@Data
public class AggStation {
private Long id;
/** 站点编码(中心侧唯一) */
private String stationCode;
private String stationName;
/** 站点 API 根地址,以 / 结尾 */
@JsonIgnore
private String baseUrl;
/** 站端门户网址 */
private String portalUrl;
@JsonIgnore
private String username;
/** 明文密码(临时;正式化前轮换并改加密/密钥) */
@JsonIgnore
private String password;
/** 1启用 0停用 */
private String enabled;
}

+ 27
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggSyncLog.java View File

@ -0,0 +1,27 @@
package com.inspect.agg.entity;
import java.time.LocalDateTime;
import lombok.Data;
/**
* 同步运行日志(表 agg_sync_log)。
*/
@Data
public class AggSyncLog {
private Long id;
private String stationCode;
/** 同步实体:area/device/point/eqpbook/channel/devicetype/task/taskplan */
private String entity;
/** FULL / ACTIVE */
private String syncType;
/** SUCCESS / FAILED */
private String status;
private Integer rowCount;
private Integer deletedCount;
private String message;
private LocalDateTime startedAt;
private LocalDateTime finishedAt;
}

+ 37
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/enums/AlgCategory.java View File

@ -0,0 +1,37 @@
package com.inspect.agg.enums;
import org.springframework.util.StringUtils;
public enum AlgCategory {
INFRARED("882", "红外"),
METER("881", "表计"),
APPEARANCE("880", "外观");
private final String code;
private final String label;
AlgCategory(String code, String label) {
this.code = code;
this.label = label;
}
public String getCode() {
return code;
}
public String getLabel() {
return label;
}
public static AlgCategory fromLabel(String label) {
if (!StringUtils.hasText(label)) {
return null;
}
for (AlgCategory category : values()) {
if (category.label.equals(label.trim())) {
return category;
}
}
throw new IllegalArgumentException("不支持的算法分类: " + label);
}
}

+ 25
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggAnalysisDefectMonthMapper.java View File

@ -0,0 +1,25 @@
package com.inspect.agg.mapper;
import java.time.LocalDate;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggAnalysisDefectMonth;
@Mapper
public interface AggAnalysisDefectMonthMapper {
int upsert(AggAnalysisDefectMonth item);
int deleteRange(@Param("stationCode") String stationCode,
@Param("beginMonth") LocalDate beginMonth,
@Param("endMonth") LocalDate endMonth);
List<AggAnalysisDefectMonth> selectRange(@Param("stationCode") String stationCode,
@Param("beginMonth") LocalDate beginMonth,
@Param("endMonth") LocalDate endMonth);
long countAll();
}

+ 30
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataAreaMapper.java View File

@ -0,0 +1,30 @@
package com.inspect.agg.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggBasedataArea;
/**
* AggBasedataArea 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggBasedataAreaMapper.xml),无 QueryWrapper。
*/
@Mapper
public interface AggBasedataAreaMapper {
/** 幂等 upsert:按 (station_code, 源主键) */
int upsert(AggBasedataArea e);
/** 全量对账:本次未出现的行标记 is_deleted=1 */
int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids);
/** 列表(限制条数,limit<=0 表示不限制) */
List<AggBasedataArea> listByStation(@Param("stationCode") String stationCode, @Param("limit") int limit);
/** 整站区域(用于组树) */
List<AggBasedataArea> listForTree(@Param("stationCode") String stationCode);
long countByStation(@Param("stationCode") String stationCode);
}

+ 26
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataDeviceMapper.java View File

@ -0,0 +1,26 @@
package com.inspect.agg.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggBasedataDevice;
/**
* AggBasedataDevice 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggBasedataDeviceMapper.xml),无 QueryWrapper。
*/
@Mapper
public interface AggBasedataDeviceMapper {
/** 幂等 upsert:按 (station_code, 源主键) */
int upsert(AggBasedataDevice e);
/** 全量对账:本次未出现的行标记 is_deleted=1 */
int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids);
List<AggBasedataDevice> list(@Param("stationCode") String stationCode, @Param("areaId") Long areaId, @Param("limit") int limit);
long count(@Param("stationCode") String stationCode, @Param("areaId") Long areaId);
}

+ 26
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataDevicetypeMapper.java View File

@ -0,0 +1,26 @@
package com.inspect.agg.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggBasedataDevicetype;
/**
* AggBasedataDevicetype 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggBasedataDevicetypeMapper.xml),无 QueryWrapper。
*/
@Mapper
public interface AggBasedataDevicetypeMapper {
/** 幂等 upsert:按 (station_code, 源主键) */
int upsert(AggBasedataDevicetype e);
/** 全量对账:本次未出现的行标记 is_deleted=1 */
int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids);
List<AggBasedataDevicetype> list(@Param("stationCode") String stationCode, @Param("limit") int limit);
long count(@Param("stationCode") String stationCode);
}

+ 28
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataEqpbookChannelMapper.java View File

@ -0,0 +1,28 @@
package com.inspect.agg.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggBasedataEqpbookChannel;
/**
* AggBasedataEqpbookChannel 闀滃儚 Mapper锛歝ustom SQL 鍏ㄩ儴鍦?XML锛坮esources/mapper/AggBasedataEqpbookChannelMapper.xml锛夛紝鏃?QueryWrapper銆? */
@Mapper
public interface AggBasedataEqpbookChannelMapper {
/** 骞傜瓑 upsert锛氭寜 (station_code, 婧愪富閿? */
int upsert(AggBasedataEqpbookChannel e);
/** 鍏ㄩ噺瀵硅处锛氭湰娆℃湭鍑虹幇鐨勮鏍囪 is_deleted=1 */
int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids);
List<AggBasedataEqpbookChannel> list(@Param("stationCode") String stationCode, @Param("limit") int limit);
long count(@Param("stationCode") String stationCode);
List<Map<String, Object>> countByDeviceSource(@Param("stationCode") String stationCode);
}

+ 26
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataEqpbookMapper.java View File

@ -0,0 +1,26 @@
package com.inspect.agg.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggBasedataEqpbook;
/**
* AggBasedataEqpbook 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggBasedataEqpbookMapper.xml),无 QueryWrapper。
*/
@Mapper
public interface AggBasedataEqpbookMapper {
/** 幂等 upsert:按 (station_code, 源主键) */
int upsert(AggBasedataEqpbook e);
/** 全量对账:本次未出现的行标记 is_deleted=1 */
int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids);
List<AggBasedataEqpbook> list(@Param("stationCode") String stationCode, @Param("limit") int limit);
long count(@Param("stationCode") String stationCode);
}

+ 31
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataPatrolpointMapper.java View File

@ -0,0 +1,31 @@
package com.inspect.agg.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.dto.AggMonitorPatrolPointVo;
import com.inspect.agg.entity.AggBasedataPatrolpoint;
/**
* AggBasedataPatrolpoint 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggBasedataPatrolpointMapper.xml),无 QueryWrapper。
*/
@Mapper
public interface AggBasedataPatrolpointMapper {
/** 幂等 upsert:按 (station_code, 源主键) */
int upsert(AggBasedataPatrolpoint e);
/** 全量对账:本次未出现的行标记 is_deleted=1 */
int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids);
List<AggBasedataPatrolpoint> list(@Param("stationCode") String stationCode, @Param("deviceId") Long deviceId,
@Param("areaId") Long areaId, @Param("limit") int limit);
long count(@Param("stationCode") String stationCode, @Param("deviceId") Long deviceId, @Param("areaId") Long areaId);
List<AggMonitorPatrolPointVo> selectMonitorRelationList(@Param("stationCode") String stationCode,
@Param("deviceId") Long deviceId);
}

+ 25
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggDefectRecordMapper.java View File

@ -0,0 +1,25 @@
package com.inspect.agg.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggDefectRecord;
@Mapper
public interface AggDefectRecordMapper {
int upsert(AggDefectRecord record);
List<AggDefectRecord> selectActiveKeys(@Param("stationCode") String stationCode);
int markDeletedByIds(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids);
List<AggDefectRecord> selectList(@Param("stationCode") String stationCode,
@Param("pointName") String pointName,
@Param("patrolTimeBegin") String patrolTimeBegin,
@Param("patrolTimeEnd") String patrolTimeEnd,
@Param("algName") String algName);
List<String> selectDistinctAlgNames(@Param("stationCode") String stationCode);
}

+ 26
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggInspectionReportMapper.java View File

@ -0,0 +1,26 @@
package com.inspect.agg.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggInspectionReport;
@Mapper
public interface AggInspectionReportMapper {
int upsert(AggInspectionReport report);
int markDeletedExcept(@Param("stationCode") String stationCode, @Param("lineIds") List<Long> lineIds);
List<AggInspectionReport> selectList(@Param("stationCode") String stationCode,
@Param("cycleStartTime") String cycleStartTime,
@Param("cycleEndTime") String cycleEndTime,
@Param("inspectionTaskName") String inspectionTaskName);
long count(@Param("stationCode") String stationCode,
@Param("cycleStartTime") String cycleStartTime,
@Param("cycleEndTime") String cycleEndTime,
@Param("inspectionTaskName") String inspectionTaskName);
}

+ 31
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggPatrolTaskMapper.java View File

@ -0,0 +1,31 @@
package com.inspect.agg.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggPatrolTask;
/**
* AggPatrolTask 镜像 Mapper:custom SQL 全部在 XML(resources/mapper/AggPatrolTaskMapper.xml),无 QueryWrapper。
*/
@Mapper
public interface AggPatrolTaskMapper {
/** 幂等 upsert:按 (station_code, 源主键) */
int upsert(AggPatrolTask e);
/** 全量对账:本次未出现的行标记 is_deleted=1 */
int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids);
List<AggPatrolTask> list(@Param("stationCode") String stationCode, @Param("limit") int limit);
long count(@Param("stationCode") String stationCode);
List<AggPatrolTask> selectTaskList(@Param("stationCode") String stationCode,
@Param("taskName") String taskName);
List<AggPatrolTask> selectEnabledTasks(@Param("stationCode") String stationCode);
}

+ 46
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggPatrolTaskStatusMapper.java View File

@ -0,0 +1,46 @@
package com.inspect.agg.mapper;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggPatrolTaskStatus;
@Mapper
public interface AggPatrolTaskStatusMapper {
int upsert(AggPatrolTaskStatus e);
int markDeletedExcept(@Param("stationCode") String stationCode, @Param("ids") List<Long> ids);
List<AggPatrolTaskStatus> list(@Param("stationCode") String stationCode,
@Param("taskState") String taskState,
@Param("limit") int limit);
long count(@Param("stationCode") String stationCode, @Param("taskState") String taskState);
List<AggPatrolTaskStatus> selectTaskStatusList(@Param("stationCode") String stationCode,
@Param("taskName") String taskName,
@Param("beginTime") String beginTime,
@Param("endTime") String endTime);
List<Map<String, Object>> countMonthlyStatsByYear(@Param("stationCode") String stationCode,
@Param("year") Integer year);
List<AggPatrolTaskStatus> selectDailyStatsByYearAndMonth(@Param("stationCode") String stationCode,
@Param("year") Integer year,
@Param("month") Integer month);
List<AggPatrolTaskStatus> selectDailyStatsByYearAndMonthAndDay(@Param("stationCode") String stationCode,
@Param("year") Integer year,
@Param("month") Integer month,
@Param("day") Integer day);
Map<String, Object> selectInspectionTaskStats(@Param("stationCode") String stationCode,
@Param("pointTypeCode") String pointTypeCode,
@Param("beginTime") LocalDateTime beginTime,
@Param("endTime") LocalDateTime endTime);
}

+ 22
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggResultAnalysisMapper.java View File

@ -0,0 +1,22 @@
package com.inspect.agg.mapper;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggResultAnalysis;
@Mapper
public interface AggResultAnalysisMapper {
int upsert(AggResultAnalysis e);
List<Map<String, Object>> selectDefectStats(@Param("stationCode") String stationCode,
@Param("pointTypeCode") String pointTypeCode,
@Param("dateType") String dateType,
@Param("beginTime") LocalDateTime beginTime,
@Param("endTime") LocalDateTime endTime);
}

+ 24
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggStationMapper.java View File

@ -0,0 +1,24 @@
package com.inspect.agg.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.inspect.agg.entity.AggStation;
/**
* 站点配置 Mapper:custom SQL 在 XML,无 QueryWrapper。
*/
@Mapper
public interface AggStationMapper {
List<AggStation> selectEnabled();
List<AggStation> selectAllOrdered();
AggStation selectByCode(@Param("stationCode") String stationCode);
List<AggStation> selectByEnabled(@Param("enabled") Integer enabled);
}

+ 14
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggSyncLogMapper.java View File

@ -0,0 +1,14 @@
package com.inspect.agg.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.inspect.agg.entity.AggSyncLog;
/**
* 同步日志 Mapper:SQL 在 XML(AggSyncLogMapper.xml),纯 MyBatis。
*/
@Mapper
public interface AggSyncLogMapper {
int insert(AggSyncLog log);
}

+ 12
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/BasedataAlgSubtypeMapper.java View File

@ -0,0 +1,12 @@
package com.inspect.agg.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.inspect.agg.entity.AggAlgSubtype;
@Mapper
public interface BasedataAlgSubtypeMapper {
List<AggAlgSubtype> selectEnabledList();
}

+ 61
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/BaseSyncScheduler.java View File

@ -0,0 +1,61 @@
package com.inspect.agg.scheduler;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.config.SyncDefaults;
import com.inspect.agg.service.AreaSyncService;
import com.inspect.agg.service.ChannelSyncService;
import com.inspect.agg.service.DevicetypeSyncService;
import com.inspect.agg.service.DeviceSyncService;
import com.inspect.agg.service.EqpbookSyncService;
import com.inspect.agg.service.InspectionReportSyncService;
import com.inspect.agg.service.PointSyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 基础数据(区域/设备/点位/台账/通道/设备类型)轮询调度:定时周期执行 + 可选启动时先同步一次。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class BaseSyncScheduler implements ApplicationRunner {
private final AreaSyncService areaSyncService;
private final DeviceSyncService deviceSyncService;
private final PointSyncService pointSyncService;
private final EqpbookSyncService eqpbookSyncService;
private final ChannelSyncService channelSyncService;
private final DevicetypeSyncService devicetypeSyncService;
private final InspectionReportSyncService inspectionReportSyncService;
private final AggProperties props;
@Scheduled(initialDelayString = SyncDefaults.BASE_INTERVAL_MS,
fixedDelayString = SyncDefaults.BASE_INTERVAL_MS)
public void scheduledBaseSync() {
syncAll();
}
@Override
public void run(ApplicationArguments args) {
if (props.getSync().getBase().isRunOnStart()) {
log.info("run-on-start enabled, start base-data sync once");
syncAll();
}
}
private void syncAll() {
areaSyncService.syncAllStations();
deviceSyncService.syncAllStations();
pointSyncService.syncAllStations();
eqpbookSyncService.syncAllStations();
channelSyncService.syncAllStations();
devicetypeSyncService.syncAllStations();
inspectionReportSyncService.syncAllStations();
}
}

+ 28
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/DefectRecordSyncScheduler.java View File

@ -0,0 +1,28 @@
package com.inspect.agg.scheduler;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.inspect.agg.config.SyncDefaults;
import com.inspect.agg.service.DefectRecordSyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@RequiredArgsConstructor
public class DefectRecordSyncScheduler {
private final DefectRecordSyncService defectRecordSyncService;
@Scheduled(initialDelay = 120_000L, fixedDelayString = SyncDefaults.BASE_INTERVAL_MS)
public void syncRecent() {
defectRecordSyncService.syncRecentAllStations();
}
@Scheduled(cron = "0 10 3 * * ?")
public void syncFull() {
defectRecordSyncService.syncFullAllStations();
}
}

+ 53
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/DynamicSyncScheduler.java View File

@ -0,0 +1,53 @@
package com.inspect.agg.scheduler;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.config.SyncDefaults;
import com.inspect.agg.service.PatrolTaskSyncService;
import com.inspect.agg.service.TaskExecSyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 动态数据调度:
* - 任务执行 ACTIVE:interval-ms(默认 60s)拉执行中子集
* - FULL:full-interval-ms(默认 5min)任务执行 + 任务方案全量对账
* 站点间并行(服务内 parallelStream)。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicSyncScheduler implements ApplicationRunner {
private final TaskExecSyncService taskExecSyncService;
private final PatrolTaskSyncService patrolTaskSyncService;
private final AggProperties props;
@Scheduled(initialDelayString = SyncDefaults.DYNAMIC_INTERVAL_MS,
fixedDelayString = SyncDefaults.DYNAMIC_INTERVAL_MS)
public void scheduledActive() {
taskExecSyncService.syncActiveAllStations();
}
@Scheduled(initialDelayString = SyncDefaults.DYNAMIC_FULL_INTERVAL_MS,
fixedDelayString = SyncDefaults.DYNAMIC_FULL_INTERVAL_MS)
public void scheduledFull() {
taskExecSyncService.syncFullAllStations();
patrolTaskSyncService.syncFullAllStations();
}
@Override
public void run(ApplicationArguments args) {
if (props.getSync().getDynamic().isRunOnStart()) {
log.info("dynamic run-on-start enabled, run ACTIVE+FULL once");
taskExecSyncService.syncActiveAllStations();
taskExecSyncService.syncFullAllStations();
patrolTaskSyncService.syncFullAllStations();
}
}
}

+ 31
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/RealtimeViewerCleanupScheduler.java View File

@ -0,0 +1,31 @@
package com.inspect.agg.scheduler;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.inspect.agg.video.RealtimeStreamService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 清理过期的实时观看票据;票据全部过期后关闭对应汇聚和站端实时流。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RealtimeViewerCleanupScheduler {
private static final long CLEAN_INTERVAL_MS = 30L * 60 * 1000;
private final RealtimeStreamService realtimeStreamService;
@Scheduled(fixedDelay = CLEAN_INTERVAL_MS, initialDelay = CLEAN_INTERVAL_MS)
public void cleanup() {
try {
realtimeStreamService.cleanupExpired();
} catch (Exception e) {
log.error("cleanup expired realtime streams failed: {}", e.getMessage(), e);
}
}
}

+ 33
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/ResultAnalysisSyncScheduler.java View File

@ -0,0 +1,33 @@
package com.inspect.agg.scheduler;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.service.AggAnalysisDefectMonthSyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@RequiredArgsConstructor
public class ResultAnalysisSyncScheduler implements ApplicationRunner {
private final AggAnalysisDefectMonthSyncService syncService;
private final AggProperties props;
@Scheduled(cron = "0 30 2 * * ?")
public void syncLast12Months() {
syncService.syncLast12MonthsAllStations();
}
@Override
public void run(ApplicationArguments args) {
if (props.getSync().getAnalysisSummary().isRunOnStart()) {
syncService.syncLast12MonthsAllStations();
}
}
}

+ 100
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggAnalysisDefectMonthSyncService.java View File

@ -0,0 +1,100 @@
package com.inspect.agg.service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.entity.AggAnalysisDefectMonth;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggAnalysisDefectMonthMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class AggAnalysisDefectMonthSyncService {
private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM");
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggAnalysisDefectMonthMapper defectMonthMapper;
private final SyncLogWriter syncLogWriter;
public void syncLast12MonthsAllStations() {
stationRegistry.listEnabled().parallelStream().forEach(this::syncLast12Months);
}
public void syncLast12Months(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
String message = null;
YearMonth current = YearMonth.now();
YearMonth first = current.minusMonths(11);
LocalDate begin = first.atDay(1);
LocalDate endExclusive = current.plusMonths(1).atDay(1);
try {
JsonNode root = stationApiClient.fetchAggDefectMonth(
station, first.format(MONTH_FORMAT), current.format(MONTH_FORMAT));
if (root.path("code").asInt() != 200) {
throw new IllegalStateException(root.path("msg").asText(root.toString()));
}
JsonNode data = root.path("data");
if (!data.isArray()) {
throw new IllegalStateException("station response data is not array: " + root);
}
defectMonthMapper.deleteRange(station.getStationCode(), begin, endExclusive);
for (JsonNode row : data) {
AggAnalysisDefectMonth item = map(row, station);
if (item == null) {
continue;
}
defectMonthMapper.upsert(item);
upserted++;
}
message = "rows=" + data.size() + ", upserted=" + upserted;
log.info("station={} analysis month summary ok: {}", station.getStationCode(), message);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} analysis month summary failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "analysis-month", "FULL", status,
upserted, 0, message, started, LocalDateTime.now());
}
private AggAnalysisDefectMonth map(JsonNode row, AggStation station) {
String statMonth = Rows.text(row, "statMonth");
if (!StringUtils.hasText(statMonth)) {
return null;
}
AggAnalysisDefectMonth item = new AggAnalysisDefectMonth();
item.setStationCode(station.getStationCode());
item.setStatMonth(YearMonth.parse(statMonth.trim(), MONTH_FORMAT).atDay(1));
String pointType = Rows.text(row, "pointType");
item.setPointType(StringUtils.hasText(pointType) ? pointType.trim() : "");
item.setMinorCount(value(row, "minorCount"));
item.setGeneralCount(value(row, "generalCount"));
item.setSeriousCount(value(row, "seriousCount"));
item.setUrgentCount(value(row, "urgentCount"));
item.setNormalCount(value(row, "normalCount"));
item.setSyncedAt(LocalDateTime.now());
return item;
}
private Integer value(JsonNode row, String key) {
Integer value = Rows.intVal(row, key);
return value == null ? 0 : value;
}
}

+ 87
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggDeviceTreeService.java View File

@ -0,0 +1,87 @@
package com.inspect.agg.service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.station.StationApiClient;
import lombok.RequiredArgsConstructor;
/**
* 关系树数据尚未展开为汇聚表,按站点读取关系树并统一补充 stationCode。
*/
@Service
@RequiredArgsConstructor
public class AggDeviceTreeService {
private static final String TREE_PATH =
"basedata/prodevmnt/device/treeAreaDeviceSelectFilterByPreset/relation";
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggProperties props;
public List<JsonNode> loadRelationTree(String stationCode, Map<String, String> params) {
List<AggStation> stations;
if (StringUtils.hasText(stationCode)) {
stations = Collections.singletonList(stationRegistry.requireByCode(stationCode));
} else {
stations = stationRegistry.listEnabled();
}
boolean aggregate = !StringUtils.hasText(stationCode);
List<JsonNode> result = new ArrayList<>();
int pageSize = props.getSync().getBase().getPageSize();
for (AggStation station : stations) {
List<JsonNode> roots = stationApiClient.fetchAllRows(
station, TREE_PATH, pageSize, 20, params, "dataId");
for (JsonNode root : roots) {
ObjectNode copy = (ObjectNode) root.deepCopy();
normalizeNode(copy, station.getStationCode(),
station.getStationName() != null ? station.getStationName() : station.getStationCode(),
aggregate);
result.add(copy);
}
}
return result;
}
private static void normalizeNode(ObjectNode node, String stationCode, String stationName,
boolean prefixIds) {
if (node == null || !node.isObject()) {
return;
}
node.put("stationCode", stationCode);
node.put("stationName", stationName != null ? stationName : stationCode);
if (prefixIds) {
String dataId = text(node, "dataId");
if (StringUtils.hasText(dataId)) {
node.put("dataId", stationCode + ":" + dataId);
}
String parentId = text(node, "parentId");
if (StringUtils.hasText(parentId)) {
node.put("parentId", stationCode + ":" + parentId);
}
}
JsonNode children = node.get("children");
if (children != null && children.isArray()) {
for (JsonNode child : children) {
normalizeNode((ObjectNode) child, stationCode, stationName, prefixIds);
}
}
}
private static String text(JsonNode node, String key) {
JsonNode value = node.get(key);
return value == null || value.isNull() ? null : value.asText();
}
}

+ 236
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggStatisticService.java View File

@ -0,0 +1,236 @@
package com.inspect.agg.service;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.inspect.agg.entity.AggAnalysisDefectMonth;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.enums.AlgCategory;
import com.inspect.agg.mapper.AggAnalysisDefectMonthMapper;
import com.inspect.agg.mapper.AggStationMapper;
import com.inspect.agg.mapper.AggPatrolTaskStatusMapper;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class AggStatisticService {
private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM");
private static final String MONTH = "month";
private static final String QUARTER = "quarter";
private final AggAnalysisDefectMonthMapper defectMonthMapper;
private final AggPatrolTaskStatusMapper taskStatusMapper;
private final AggStationMapper stationMapper;
public List<Map<String, Object>> defectStatistic(String stationCode, String algCategory,
String dateType) {
AlgCategory category = AlgCategory.fromLabel(algCategory);
String normalizedType = normalizeDateType(dateType);
YearMonth current = YearMonth.now();
LocalDate begin;
LocalDate endExclusive;
if (QUARTER.equals(normalizedType)) {
YearMonth currentQuarter = quarterStart(current);
begin = currentQuarter.minusMonths(9).atDay(1);
endExclusive = currentQuarter.plusMonths(3).atDay(1);
} else {
begin = current.minusMonths(11).atDay(1);
endExclusive = current.plusMonths(1).atDay(1);
}
List<AggAnalysisDefectMonth> rows = defectMonthMapper.selectRange(stationCode, begin, endExclusive);
Map<YearMonth, Counts> monthlyMap = toMonthlyMap(rows, category == null ? "" : category.getCode());
String stationName = stationDisplayName(stationCode);
List<Map<String, Object>> result = new ArrayList<>();
if (QUARTER.equals(normalizedType)) {
YearMonth firstQuarter = quarterStart(current).minusMonths(9);
for (int i = 0; i < 4; i++) {
YearMonth quarter = firstQuarter.plusMonths(i * 3L);
Counts counts = new Counts();
for (int monthOffset = 0; monthOffset < 3; monthOffset++) {
counts.add(monthlyMap.get(quarter.plusMonths(monthOffset)));
}
result.add(toPeriodResult(counts,
quarter.getYear() + "-Q" + ((quarter.getMonthValue() - 1) / 3 + 1),
normalizedType, algCategory, stationCode, stationName));
}
return result;
}
YearMonth first = current.minusMonths(11);
for (int i = 0; i < 12; i++) {
YearMonth month = first.plusMonths(i);
Counts counts = monthlyMap.getOrDefault(month, new Counts());
result.add(toPeriodResult(counts, month.format(MONTH_FORMAT), normalizedType, algCategory,
stationCode, stationName));
}
return result;
}
public List<Map<String, Object>> inspectionStatistic(String stationCode, String algCategory) {
AlgCategory category = AlgCategory.fromLabel(algCategory);
String stationName = stationDisplayName(stationCode);
if (category != null) {
return java.util.Collections.singletonList(inspectionByCategory(stationCode, stationName, category));
}
List<Map<String, Object>> result = new ArrayList<>();
for (AlgCategory item : AlgCategory.values()) {
result.add(inspectionByCategory(stationCode, stationName, item));
}
return result;
}
private Map<String, Object> inspectionByCategory(String stationCode, String stationName,
AlgCategory category) {
YearMonth current = YearMonth.now();
LocalDate begin = current.atDay(1);
LocalDate endExclusive = current.plusMonths(1).atDay(1);
Map<String, Object> taskStats = taskStatusMapper.selectInspectionTaskStats(
stationCode, category.getCode(), begin.atStartOfDay(), endExclusive.atStartOfDay());
List<AggAnalysisDefectMonth> rows = defectMonthMapper.selectRange(stationCode, begin, endExclusive);
Counts defectStats = new Counts();
for (AggAnalysisDefectMonth row : rows) {
if (category.getCode().equals(emptyIfNull(row.getPointType()))) {
defectStats.add(row);
}
}
return toInspectionResult(category.getLabel(), taskStats, defectStats, stationCode, stationName);
}
private Map<YearMonth, Counts> toMonthlyMap(List<AggAnalysisDefectMonth> rows, String pointType) {
Map<YearMonth, Counts> result = new LinkedHashMap<>();
for (AggAnalysisDefectMonth row : rows) {
if (!pointType.equals(emptyIfNull(row.getPointType()))) {
continue;
}
YearMonth month = YearMonth.from(row.getStatMonth());
result.computeIfAbsent(month, key -> new Counts()).add(row);
}
return result;
}
private Map<String, Object> toPeriodResult(Counts counts, String period,
String dateType, String algCategory,
String stationCode, String stationName) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("date", period);
item.put("dateType", dateType);
if (StringUtils.hasText(algCategory)) {
item.put("algCategory", algCategory);
}
if (StringUtils.hasText(stationCode)) {
item.put("stationCode", stationCode);
item.put("stationName", stationName != null ? stationName : stationCode);
}
item.put("minorCount", counts.minor);
item.put("generalCount", counts.general);
item.put("seriousCount", counts.serious);
item.put("urgentCount", counts.urgent);
item.put("normalCount", counts.normal);
item.put("defectCount", counts.defectCount());
return item;
}
private Map<String, Object> toInspectionResult(String label, Map<String, Object> taskStats,
Counts defectStats,
String stationCode, String stationName) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("algCategory", label);
if (StringUtils.hasText(stationCode)) {
result.put("stationCode", stationCode);
result.put("stationName", stationName != null ? stationName : stationCode);
}
result.put("inspectedTaskCount", number(taskStats, "inspected_task_count"));
result.put("completedTaskCount", number(taskStats, "completed_task_count"));
result.put("defectCount", defectStats.defectCount());
result.put("minorCount", defectStats.minor);
result.put("generalCount", defectStats.general);
result.put("seriousCount", defectStats.serious);
result.put("urgentCount", defectStats.urgent);
return result;
}
private String stationDisplayName(String stationCode) {
if (!StringUtils.hasText(stationCode)) {
return null;
}
AggStation station = stationMapper.selectByCode(stationCode);
if (station != null && StringUtils.hasText(station.getStationName())) {
return station.getStationName();
}
return stationCode;
}
private static String normalizeDateType(String dateType) {
return QUARTER.equalsIgnoreCase(dateType == null ? "" : dateType.trim()) ? QUARTER : MONTH;
}
private static YearMonth quarterStart(YearMonth month) {
int quarterMonth = ((month.getMonthValue() - 1) / 3) * 3 + 1;
return YearMonth.of(month.getYear(), quarterMonth);
}
private static long number(Map<String, Object> row, String key) {
if (row == null) {
return 0L;
}
Object value = row.get(key);
if (value == null) {
value = row.get(key.toUpperCase());
}
return value instanceof Number ? ((Number) value).longValue() : 0L;
}
private static String emptyIfNull(String value) {
return value == null ? "" : value;
}
private static class Counts {
private int minor;
private int general;
private int serious;
private int urgent;
private int normal;
private void add(AggAnalysisDefectMonth row) {
if (row == null) {
return;
}
minor += nz(row.getMinorCount());
general += nz(row.getGeneralCount());
serious += nz(row.getSeriousCount());
urgent += nz(row.getUrgentCount());
normal += nz(row.getNormalCount());
}
private void add(Counts other) {
if (other == null) {
return;
}
minor += other.minor;
general += other.general;
serious += other.serious;
urgent += other.urgent;
normal += other.normal;
}
private long defectCount() {
return minor + general + serious + urgent;
}
private static int nz(Integer value) {
return value == null ? 0 : value;
}
}
}

+ 99
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AreaSyncService.java View File

@ -0,0 +1,99 @@
package com.inspect.agg.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggBasedataArea;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggBasedataAreaMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 区域同步服务(基础数据全量:源 basedata_area)。站点配置来自 DB(StationRegistry)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AreaSyncService {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggBasedataAreaMapper aggAreaMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
public void syncAllStations() {
for (AggStation station : stationRegistry.listEnabled()) {
syncStation(station);
}
}
public void syncStation(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
int deletedMarked = 0;
String message = null;
try {
List<JsonNode> rows = stationApiClient.fetchAllAreas(station,
props.getSync().getBase().getPageSize(), 500);
List<Long> ids = new ArrayList<>();
for (JsonNode row : rows) {
AggBasedataArea area = map(row, station);
if (area == null) {
continue;
}
aggAreaMapper.upsert(area);
if (area.getAreaId() != null) {
ids.add(area.getAreaId());
}
upserted++;
}
deletedMarked = aggAreaMapper.markDeletedExcept(station.getStationCode(), ids);
message = "rows=" + rows.size();
log.info("station={} area sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} area sync failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "area", "FULL", status, upserted, deletedMarked,
message, started, LocalDateTime.now());
}
private AggBasedataArea map(JsonNode r, AggStation station) {
Long areaId = Rows.longVal(r, "areaId", "area_id");
if (areaId == null) {
return null;
}
AggBasedataArea a = new AggBasedataArea();
a.setStationCode(station.getStationCode());
a.setAreaId(areaId);
Long parentId = Rows.longVal(r, "parentId", "parent_id", "pid");
a.setParentId(parentId == null ? 0L : parentId);
a.setAncestors(Rows.text(r, "ancestors"));
a.setAreaName(Rows.text(r, "areaName", "area_name"));
a.setAreaCode(Rows.text(r, "areaCode", "area_code"));
a.setAreaAddr(Rows.text(r, "areaAddr", "area_addr"));
a.setAreaType(Rows.text(r, "areaType", "area_type"));
a.setDeviceTypeCode(Rows.text(r, "deviceTypeCode", "device_type_code"));
Integer orderNum = Rows.intVal(r, "orderNum", "order_num");
a.setOrderNum(orderNum == null ? 0 : orderNum);
a.setStatus(Rows.text(r, "status"));
a.setRemark(Rows.text(r, "remark"));
a.setUpdateTime(Rows.date(r, "updateTime", "update_time"));
a.setSyncedAt(LocalDateTime.now());
a.setIsDeleted(0);
return a;
}
}

+ 100
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/ChannelSyncService.java View File

@ -0,0 +1,100 @@
package com.inspect.agg.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggBasedataEqpbookChannel;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggBasedataEqpbookChannelMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 设备通道同步服务(基础数据全量:源 basedata_eqpbook_channel)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ChannelSyncService {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggBasedataEqpbookChannelMapper aggChannelMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
public void syncAllStations() {
for (AggStation station : stationRegistry.listEnabled()) {
syncStation(station);
}
}
public void syncStation(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
int deletedMarked = 0;
String message = null;
try {
List<JsonNode> rows = stationApiClient.fetchAllChannels(station,
props.getSync().getBase().getPageSize(), 100);
List<Long> ids = new ArrayList<>();
for (JsonNode row : rows) {
AggBasedataEqpbookChannel c = map(row, station);
if (c == null) {
continue;
}
aggChannelMapper.upsert(c);
if (c.getChannelId() != null) {
ids.add(c.getChannelId());
}
upserted++;
}
deletedMarked = aggChannelMapper.markDeletedExcept(station.getStationCode(), ids);
message = "rows=" + rows.size();
log.info("station={} channel sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} channel sync failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "channel", "FULL", status, upserted, deletedMarked,
message, started, LocalDateTime.now());
}
private AggBasedataEqpbookChannel map(JsonNode r, AggStation station) {
Long id = Rows.longVal(r, "channelId");
if (id == null) {
return null;
}
AggBasedataEqpbookChannel c = new AggBasedataEqpbookChannel();
c.setStationCode(station.getStationCode());
c.setChannelId(id);
c.setChannelCode(Rows.text(r, "channelCode"));
c.setChannelName(Rows.text(r, "channelName"));
c.setChannelType(Rows.text(r, "channelType"));
c.setOnlineState(Rows.text(r, "onlineState"));
c.setChannelContent(Rows.text(r, "channelContent"));
c.setHost(Rows.text(r, "host"));
c.setPort(Rows.text(r, "port"));
c.setAreaId(Rows.longVal(r, "areaId"));
c.setAreaName(Rows.text(r, "areaName"));
c.setEqpbookId(Rows.longVal(r, "eqpbookId"));
c.setPatrolDeviceCode(Rows.text(r, "patroldeviceCode", "patrolDeviceCode"));
c.setUpSystemDeviceCode(Rows.text(r, "upSystemDeviceCode"));
c.setDeviceSource(Rows.text(r, "deviceSource"));
c.setUpdateTime(Rows.date(r, "updateTime"));
c.setSyncedAt(LocalDateTime.now());
c.setIsDeleted(0);
return c;
}
}

+ 185
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DefectRecordSyncService.java View File

@ -0,0 +1,185 @@
package com.inspect.agg.service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggDefectRecord;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggDefectRecordMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Syncs merged defect-record rows from station-side /patrol/analysis/defect/list.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DefectRecordSyncService {
private static final String PATH = "patrol/analysis/defect/list";
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggDefectRecordMapper defectRecordMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
public void syncFullAllStations() {
stationRegistry.listEnabled().parallelStream().forEach(this::syncFull);
}
public void syncRecentAllStations() {
stationRegistry.listEnabled().parallelStream().forEach(this::syncRecent);
}
public void syncFull(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
int deletedMarked = 0;
String message = null;
try {
List<AggDefectRecord> existing = defectRecordMapper.selectActiveKeys(station.getStationCode());
Map<String, Long> existingIds = existing.stream()
.collect(Collectors.toMap(this::naturalKey, AggDefectRecord::getId, (oldValue, ignored) -> oldValue));
List<JsonNode> rows = stationApiClient.fetchAllRows(station, PATH,
props.getSync().getBase().getPageSize(), 1000, "lineId");
Set<String> presentKeys = new HashSet<>();
for (JsonNode row : rows) {
AggDefectRecord record = map(row, station);
if (record == null) {
continue;
}
defectRecordMapper.upsert(record);
presentKeys.add(naturalKey(record));
upserted++;
}
List<Long> missingIds = existingIds.entrySet().stream()
.filter(entry -> !presentKeys.contains(entry.getKey()))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
if (!missingIds.isEmpty()) {
deletedMarked = defectRecordMapper.markDeletedByIds(station.getStationCode(), missingIds);
}
message = "rows=" + rows.size() + ", deletedMarked=" + deletedMarked;
log.info("station={} defect record FULL ok: upserted={}, deletedMarked={}",
station.getStationCode(), upserted, deletedMarked);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} defect record FULL failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "defect", "FULL", status, upserted, deletedMarked,
message, started, LocalDateTime.now());
}
public void syncRecent(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
String message = null;
try {
Map<String, String> query = new HashMap<>();
query.put("patrolTimeBegin", LocalDate.now().minusDays(7).toString());
query.put("patrolTimeEnd", LocalDate.now().toString());
List<JsonNode> rows = stationApiClient.fetchAllRows(station, PATH,
props.getSync().getBase().getPageSize(), 200, query, "lineId");
for (JsonNode row : rows) {
AggDefectRecord record = map(row, station);
if (record != null) {
defectRecordMapper.upsert(record);
upserted++;
}
}
message = "rows=" + rows.size();
log.info("station={} defect record RECENT ok: upserted={}", station.getStationCode(), upserted);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} defect record RECENT failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "defect", "RECENT", status, upserted, 0,
message, started, LocalDateTime.now());
}
private AggDefectRecord map(JsonNode r, AggStation station) {
String taskPatrolId = text(r, "taskPatrolId");
String objectId = text(r, "objectId");
String algType = text(r, "algType");
if (isBlank(taskPatrolId) || isBlank(objectId) || isBlank(algType)) {
return null;
}
AggDefectRecord e = new AggDefectRecord();
e.setStationCode(station.getStationCode());
e.setDeviceName(text(r, "deviceName"));
e.setChannelName(text(r, "channelName"));
e.setChannelCode(text(r, "channelCode"));
e.setDeviceSource(text(r, "deviceSource"));
e.setPointName(text(r, "pointName"));
e.setPointId(text(r, "pointId"));
e.setWarnStatus(text(r, "warnStatus"));
e.setAlgName(text(r, "algName"));
e.setResValue(text(r, "resValue"));
e.setImg(text(r, "img"));
e.setFilterDesc(text(r, "filterDesc"));
e.setBrightDesc(text(r, "brightDesc"));
e.setImgAnalyse(text(r, "imgAnalyse"));
e.setBrightImgAnalyse(text(r, "brightImgAnalyse"));
e.setDesc(text(r, "desc"));
e.setLineId(text(r, "lineId"));
e.setPatrolTime(Rows.date(r, "patrolTime"));
e.setPointStatus(text(r, "pointStatus"));
e.setImgType(text(r, "imgType"));
e.setDataType(text(r, "dataType"));
e.setFilter(text(r, "filter"));
e.setRequestId(text(r, "requestId"));
e.setResStatus(text(r, "resStatus"));
e.setBrightResStatus(text(r, "brightResStatus"));
e.setAreaName(text(r, "areaName"));
e.setAreaId(text(r, "areaId"));
e.setAlgType(algType);
e.setObjectId(objectId);
e.setImageNormalUrlPath(text(r, "imageNormalUrlPath"));
e.setFilePath(text(r, "filePath"));
e.setSuggestion(text(r, "suggestion"));
e.setOriginalValue(text(r, "originalValue"));
e.setTaskPatrolId(taskPatrolId);
e.setTaskName(text(r, "taskName"));
Integer defectLevel = Rows.intVal(r, "defectLevel");
e.setDefectLevel(defectLevel == null ? 0 : defectLevel);
e.setSyncedAt(LocalDateTime.now());
e.setIsDeleted(0);
return e;
}
private String naturalKey(AggDefectRecord r) {
return r.getStationCode() + "|" + r.getTaskPatrolId() + "|" + r.getObjectId() + "|" + r.getAlgType();
}
private static String text(JsonNode node, String key) {
JsonNode value = node.get(key);
if (value == null || value.isNull()) {
return null;
}
return value.asText();
}
private static boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
}

+ 103
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DeviceSyncService.java View File

@ -0,0 +1,103 @@
package com.inspect.agg.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggBasedataDevice;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggBasedataDeviceMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 设备同步服务(基础数据全量:源 basedata_device)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceSyncService {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggBasedataDeviceMapper aggDeviceMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
public void syncAllStations() {
for (AggStation station : stationRegistry.listEnabled()) {
syncStation(station);
}
}
public void syncStation(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
int deletedMarked = 0;
String message = null;
try {
List<JsonNode> rows = stationApiClient.fetchAllDevices(station,
props.getSync().getBase().getPageSize(), 500);
List<Long> ids = new ArrayList<>();
for (JsonNode row : rows) {
AggBasedataDevice d = map(row, station);
if (d == null) {
continue;
}
aggDeviceMapper.upsert(d);
if (d.getDeviceId() != null) {
ids.add(d.getDeviceId());
}
upserted++;
}
deletedMarked = aggDeviceMapper.markDeletedExcept(station.getStationCode(), ids);
message = "rows=" + rows.size();
log.info("station={} device sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} device sync failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "device", "FULL", status, upserted, deletedMarked,
message, started, LocalDateTime.now());
}
private AggBasedataDevice map(JsonNode r, AggStation station) {
Long deviceId = Rows.longVal(r, "deviceId");
if (deviceId == null) {
return null;
}
AggBasedataDevice d = new AggBasedataDevice();
d.setStationCode(station.getStationCode());
d.setDeviceId(deviceId);
Long parentId = Rows.longVal(r, "parentId");
d.setParentId(parentId == null ? 0L : parentId);
d.setAncestors(Rows.text(r, "ancestors"));
d.setAreaId(Rows.longVal(r, "areaId"));
d.setAreaName(Rows.text(r, "areaName"));
d.setDeviceCode(Rows.text(r, "deviceCode"));
d.setDeviceName(Rows.text(r, "deviceName"));
d.setDeviceTypeName(Rows.text(r, "deviceTypeName"));
d.setDeviceModel(Rows.text(r, "deviceModel"));
d.setManufacturer(Rows.text(r, "manufacturer"));
d.setPowerMainEquFlag(Rows.text(r, "powerMainEquFlag"));
d.setStationNo(Rows.text(r, "stationNo"));
d.setMonitorIndexNo(Rows.text(r, "monitorIndexNo"));
d.setStatus(Rows.text(r, "status"));
Integer orderNum = Rows.intVal(r, "orderNum");
d.setOrderNum(orderNum == null ? 0 : orderNum);
d.setUpdateTime(Rows.date(r, "updateTime"));
d.setSyncedAt(LocalDateTime.now());
d.setIsDeleted(0);
return d;
}
}

+ 102
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DevicetypeSyncService.java View File

@ -0,0 +1,102 @@
package com.inspect.agg.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggBasedataDevicetype;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggBasedataDevicetypeMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 设备类型同步服务(基础数据全量:源 basedata_devicetype)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DevicetypeSyncService {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggBasedataDevicetypeMapper aggDevicetypeMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
public void syncAllStations() {
for (AggStation station : stationRegistry.listEnabled()) {
syncStation(station);
}
}
public void syncStation(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
int deletedMarked = 0;
String message = null;
try {
List<JsonNode> rows = stationApiClient.fetchAllDeviceTypes(station,
props.getSync().getBase().getPageSize(), 100);
List<Long> ids = new ArrayList<>();
for (JsonNode row : rows) {
AggBasedataDevicetype d = map(row, station);
if (d == null) {
continue;
}
aggDevicetypeMapper.upsert(d);
if (d.getDeviceTypeId() != null) {
ids.add(d.getDeviceTypeId());
}
upserted++;
}
deletedMarked = aggDevicetypeMapper.markDeletedExcept(station.getStationCode(), ids);
message = "rows=" + rows.size();
log.info("station={} devicetype sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} devicetype sync failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "devicetype", "FULL", status, upserted, deletedMarked,
message, started, LocalDateTime.now());
}
private AggBasedataDevicetype map(JsonNode r, AggStation station) {
Long id = Rows.longVal(r, "devicetypeId");
if (id == null) {
return null;
}
AggBasedataDevicetype d = new AggBasedataDevicetype();
d.setStationCode(station.getStationCode());
d.setDeviceTypeId(id);
Long parentId = Rows.longVal(r, "parentId");
d.setParentId(parentId == null ? 0L : parentId);
Integer orderNum = Rows.intVal(r, "orderNum");
d.setOrderNum(orderNum == null ? 0 : orderNum);
d.setAncestors(Rows.text(r, "ancestors"));
d.setAncestorsAddr(Rows.text(r, "ancestorsAddr"));
d.setDeviceTypeName(Rows.text(r, "devicetypeName", "deviceTypeName"));
d.setDeviceTypeCode(Rows.text(r, "devicetypeCode", "deviceTypeCode"));
d.setPowerMainEquFlag(Rows.text(r, "powerMainEquFlag"));
d.setPhaseNum(Rows.text(r, "phaseNum"));
d.setPhase(Rows.text(r, "phase"));
d.setStatus(Rows.text(r, "status"));
d.setProfessionType(Rows.text(r, "professionType"));
Long level = Rows.longVal(r, "level");
d.setLevel(level == null ? 0L : level);
d.setUpdateTime(Rows.date(r, "updateTime"));
d.setSyncedAt(LocalDateTime.now());
d.setIsDeleted(0);
return d;
}
}

+ 103
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/EqpbookSyncService.java View File

@ -0,0 +1,103 @@
package com.inspect.agg.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggBasedataEqpbook;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggBasedataEqpbookMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 设备台账同步服务(基础数据全量:源 basedata_eqpbook)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class EqpbookSyncService {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggBasedataEqpbookMapper aggEqpbookMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
public void syncAllStations() {
for (AggStation station : stationRegistry.listEnabled()) {
syncStation(station);
}
}
public void syncStation(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
int deletedMarked = 0;
String message = null;
try {
List<JsonNode> rows = stationApiClient.fetchAllEqpbooks(station,
props.getSync().getBase().getPageSize(), 100);
List<Long> ids = new ArrayList<>();
for (JsonNode row : rows) {
AggBasedataEqpbook e = map(row, station);
if (e == null) {
continue;
}
aggEqpbookMapper.upsert(e);
if (e.getEqpbookId() != null) {
ids.add(e.getEqpbookId());
}
upserted++;
}
deletedMarked = aggEqpbookMapper.markDeletedExcept(station.getStationCode(), ids);
message = "rows=" + rows.size();
log.info("station={} eqpbook sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} eqpbook sync failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "eqpbook", "FULL", status, upserted, deletedMarked,
message, started, LocalDateTime.now());
}
private AggBasedataEqpbook map(JsonNode r, AggStation station) {
Long id = Rows.longVal(r, "eqpbookId");
if (id == null) {
return null;
}
AggBasedataEqpbook e = new AggBasedataEqpbook();
e.setStationCode(station.getStationCode());
e.setEqpbookId(id);
e.setPatrolDeviceCode(Rows.text(r, "patroldeviceCode", "patrolDeviceCode"));
e.setPatrolDeviceName(Rows.text(r, "patroldeviceName", "patrolDeviceName"));
e.setStationName(Rows.text(r, "stationName"));
e.setAreaId(Rows.longVal(r, "areaId"));
e.setAreaName(Rows.text(r, "areaName"));
e.setDeviceSource(Rows.text(r, "deviceSource"));
e.setDeviceModel(Rows.text(r, "deviceModel"));
e.setManufacturer(Rows.text(r, "manufacturer"));
e.setDeviceType(Rows.text(r, "type"));
e.setIpAddr(Rows.text(r, "ipAddr"));
e.setPort(Rows.text(r, "port"));
e.setVideoNvrCode(Rows.text(r, "videoNvrCode"));
e.setMainSystemCode(Rows.text(r, "mainSystemCode"));
e.setUseMode(Rows.text(r, "useMode"));
e.setVideoMode(Rows.text(r, "videoMode"));
e.setUpdateTime(Rows.date(r, "updateTime"));
e.setSyncedAt(LocalDateTime.now());
e.setIsDeleted(0);
return e;
}
}

+ 217
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/EqpbookTreeService.java View File

@ -0,0 +1,217 @@
package com.inspect.agg.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.inspect.agg.entity.AggBasedataArea;
import com.inspect.agg.entity.AggBasedataEqpbook;
import com.inspect.agg.entity.AggBasedataEqpbookChannel;
import com.inspect.agg.mapper.AggBasedataAreaMapper;
import com.inspect.agg.mapper.AggBasedataEqpbookChannelMapper;
import com.inspect.agg.mapper.AggBasedataEqpbookMapper;
import lombok.RequiredArgsConstructor;
/**
* 使用汇聚区域、台账和通道数据构建通道树。
*/
@Service
@RequiredArgsConstructor
public class EqpbookTreeService {
private final AggBasedataAreaMapper areaMapper;
private final AggBasedataEqpbookMapper eqpbookMapper;
private final AggBasedataEqpbookChannelMapper channelMapper;
public List<Map<String, Object>> buildFilterTree(String stationCode,
String patrolDeviceName,
String onlineStatus,
String patrolDeviceTypeFlag) {
List<AggBasedataArea> areas = areaMapper.listForTree(stationCode);
List<AggBasedataEqpbook> eqpbooks = eqpbookMapper.list(stationCode, 0);
List<AggBasedataEqpbookChannel> channels = channelMapper.list(stationCode, 0);
Map<String, AggBasedataArea> areaIndex = new HashMap<>();
for (AggBasedataArea area : areas) {
areaIndex.put(areaKey(area.getStationCode(), area.getAreaId()), area);
}
Map<String, List<AggBasedataEqpbookChannel>> channelIndex = new HashMap<>();
for (AggBasedataEqpbookChannel channel : channels) {
channelIndex.computeIfAbsent(eqpbookKey(channel.getStationCode(), channel.getEqpbookId()),
key -> new ArrayList<>()).add(channel);
}
List<AggBasedataEqpbook> selectedEqpbooks = new ArrayList<>();
Set<String> selectedAreaKeys = new HashSet<>();
for (AggBasedataEqpbook eqpbook : eqpbooks) {
if (!matchesName(eqpbook, patrolDeviceName) || !matchesType(eqpbook, patrolDeviceTypeFlag)) {
continue;
}
List<AggBasedataEqpbookChannel> eqpbookChannels =
channelIndex.getOrDefault(eqpbookKey(eqpbook.getStationCode(), eqpbook.getEqpbookId()),
new ArrayList<AggBasedataEqpbookChannel>());
if (eqpbookChannels.isEmpty()) {
continue;
}
boolean online = false;
for (AggBasedataEqpbookChannel channel : eqpbookChannels) {
if ("1".equals(channel.getOnlineState())) {
online = true;
break;
}
}
if ("1".equals(onlineStatus) && !online) continue;
if ("0".equals(onlineStatus) && online) continue;
AggBasedataArea area = areaIndex.get(areaKey(eqpbook.getStationCode(), eqpbook.getAreaId()));
if (area == null) {
continue;
}
selectedEqpbooks.add(eqpbook);
collectAreaKeys(area, areaIndex, selectedAreaKeys);
}
Map<String, Map<String, Object>> areaNodes = new LinkedHashMap<>();
for (String key : selectedAreaKeys) {
AggBasedataArea area = areaIndex.get(key);
if (area == null) continue;
Map<String, Object> node = new LinkedHashMap<>();
node.put("id", key);
String parentKey = area.getParentId() == null ? null : areaKey(area.getStationCode(), area.getParentId());
node.put("parentId", selectedAreaKeys.contains(parentKey) ? parentKey : null);
node.put("label", area.getAreaName());
node.put("areaId", area.getAreaId());
node.put("areaName", area.getAreaName());
node.put("areaFlag", "Yes");
node.put("areaType", area.getAreaType());
node.put("stationCode", area.getStationCode());
node.put("stationName", area.getStationName() != null ? area.getStationName() : area.getStationCode());
node.put("children", new ArrayList<Map<String, Object>>());
areaNodes.put(key, node);
}
for (AggBasedataEqpbook eqpbook : selectedEqpbooks) {
String parentKey = areaKey(eqpbook.getStationCode(), eqpbook.getAreaId());
Map<String, Object> areaNode = areaNodes.get(parentKey);
if (areaNode == null) continue;
List<AggBasedataEqpbookChannel> eqpbookChannels =
channelIndex.get(eqpbookKey(eqpbook.getStationCode(), eqpbook.getEqpbookId()));
boolean online = false;
for (AggBasedataEqpbookChannel channel : eqpbookChannels) {
if ("1".equals(channel.getOnlineState())) {
online = true;
break;
}
}
String eqpbookNodeId = eqpbookKey(eqpbook.getStationCode(), eqpbook.getEqpbookId());
Map<String, Object> eqpbookNode = new LinkedHashMap<>();
eqpbookNode.put("id", eqpbookNodeId);
eqpbookNode.put("parentId", parentKey);
eqpbookNode.put("label", eqpbook.getPatrolDeviceName());
eqpbookNode.put("areaId", eqpbook.getAreaId());
eqpbookNode.put("areaName", eqpbook.getAreaName());
eqpbookNode.put("eqpbookId", eqpbook.getEqpbookId());
eqpbookNode.put("patroldeviceFlag", "Yes");
eqpbookNode.put("patroldeviceCode", eqpbook.getPatrolDeviceCode());
eqpbookNode.put("patroldeviceName", eqpbook.getPatrolDeviceName());
eqpbookNode.put("patroldeviceType", eqpbook.getDeviceType());
eqpbookNode.put("patroldeviceOnlineStatus", online ? "1" : "0");
eqpbookNode.put("type", eqpbook.getDeviceType());
eqpbookNode.put("stationCode", eqpbook.getStationCode());
eqpbookNode.put("stationName", eqpbook.getStationName() != null ? eqpbook.getStationName() : eqpbook.getStationCode());
eqpbookNode.put("children", new ArrayList<Map<String, Object>>());
children(areaNode).add(eqpbookNode);
for (AggBasedataEqpbookChannel channel : eqpbookChannels) {
Map<String, Object> channelNode = new LinkedHashMap<>();
channelNode.put("id", eqpbookNodeId + ":channel:" + channel.getChannelId());
channelNode.put("parentId", eqpbookNodeId);
channelNode.put("label", channel.getChannelName());
channelNode.put("areaId", eqpbook.getAreaId());
channelNode.put("areaName", eqpbook.getAreaName());
channelNode.put("patroldeviceCode", eqpbook.getPatrolDeviceCode());
channelNode.put("patroldeviceName", eqpbook.getPatrolDeviceName());
channelNode.put("patroldeviceType", eqpbook.getDeviceType());
channelNode.put("patroldeviceOnlineStatus", channel.getOnlineState());
channelNode.put("channelId", channel.getChannelId());
channelNode.put("channelCode", channel.getChannelCode());
channelNode.put("channelName", channel.getChannelName());
channelNode.put("channelFlag", "Yes");
channelNode.put("channelType", channel.getChannelType());
channelNode.put("host", channel.getHost());
channelNode.put("port", channel.getPort());
channelNode.put("stationCode", channel.getStationCode());
channelNode.put("stationName", channel.getStationName() != null ? channel.getStationName() : channel.getStationCode());
children(eqpbookNode).add(channelNode);
}
}
List<Map<String, Object>> roots = new ArrayList<>();
for (Map<String, Object> node : areaNodes.values()) {
if (node.get("parentId") == null) {
roots.add(node);
} else {
Map<String, Object> parent = areaNodes.get(node.get("parentId"));
if (parent != null) {
children(parent).add(node);
} else {
roots.add(node);
}
}
}
return roots;
}
@SuppressWarnings("unchecked")
private static List<Map<String, Object>> children(Map<String, Object> node) {
return (List<Map<String, Object>>) node.get("children");
}
private static void collectAreaKeys(AggBasedataArea area,
Map<String, AggBasedataArea> areaIndex,
Set<String> keys) {
AggBasedataArea current = area;
while (current != null) {
String key = areaKey(current.getStationCode(), current.getAreaId());
if (!keys.add(key)) break;
current = current.getParentId() == null
? null
: areaIndex.get(areaKey(current.getStationCode(), current.getParentId()));
}
}
private static boolean matchesName(AggBasedataEqpbook eqpbook, String value) {
if (!StringUtils.hasText(value)) return true;
return value.equals(eqpbook.getPatrolDeviceCode())
|| (eqpbook.getPatrolDeviceName() != null
&& eqpbook.getPatrolDeviceName().contains(value));
}
private static boolean matchesType(AggBasedataEqpbook eqpbook, String flag) {
if (!StringUtils.hasText(flag)) return true;
String type = eqpbook.getDeviceType();
if ("nvr".equals(flag)) return "11".equals(type);
if ("ipc".equals(flag)) return type == null || (!"11".equals(type) && !"20".equals(type));
if ("robotAndUav".equals(flag)) return "1".equals(type) || "2".equals(type)
|| "3".equals(type) || "13".equals(type);
if ("uav".equals(flag)) return "13".equals(type);
return true;
}
private static String areaKey(String stationCode, Long areaId) {
return stationCode + ":area:" + areaId;
}
private static String eqpbookKey(String stationCode, Long eqpbookId) {
return stationCode + ":eqpbook:" + eqpbookId;
}
}

+ 104
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/InspectionReportSyncService.java View File

@ -0,0 +1,104 @@
package com.inspect.agg.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggInspectionReport;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggInspectionReportMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 巡视报告镜像同步。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class InspectionReportSyncService {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggInspectionReportMapper reportMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
public void syncAllStations() {
for (AggStation station : stationRegistry.listEnabled()) {
syncStation(station);
}
}
public void syncStation(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
int deletedMarked = 0;
String message = null;
try {
List<JsonNode> rows = stationApiClient.fetchAllInspectionReports(
station, props.getSync().getBase().getPageSize(), 100);
List<Long> lineIds = new ArrayList<>();
for (JsonNode row : rows) {
AggInspectionReport report = map(row, station);
if (report == null) {
continue;
}
reportMapper.upsert(report);
lineIds.add(report.getLineId());
upserted++;
}
deletedMarked = reportMapper.markDeletedExcept(station.getStationCode(), lineIds);
message = "rows=" + rows.size();
log.info("station={} inspection_report sync ok: upserted={}, deletedMarked={}",
station.getStationCode(), upserted, deletedMarked);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} inspection_report sync failed: {}",
station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "report", "FULL", status, upserted,
deletedMarked, message, started, LocalDateTime.now());
}
private AggInspectionReport map(JsonNode row, AggStation station) {
Long lineId = Rows.longVal(row, "lineId");
if (lineId == null) {
return null;
}
AggInspectionReport report = new AggInspectionReport();
report.setStationCode(station.getStationCode());
report.setLineId(lineId);
report.setStationName(Rows.text(row, "stationName"));
report.setVoltLevel(Rows.text(row, "voltLevel"));
report.setStationType(Rows.text(row, "stationType"));
report.setInspectionDate(Rows.date(row, "inspectionDate"));
report.setInspectionTaskName(Rows.text(row, "inspectionTaskName"));
report.setEnvInfo(Rows.text(row, "envInfo"));
report.setInspectionStartTime(Rows.date(row, "inspectionStartTime"));
report.setInspectionEndTime(Rows.date(row, "inspectionEndTime"));
report.setPatrolStatistics(Rows.text(row, "patrolStatistics"));
report.setCheckPerson(Rows.text(row, "checkPerson"));
report.setCheckTime(Rows.date(row, "checkTime"));
report.setDescription(Rows.text(row, "description"));
report.setFilePath(Rows.text(row, "filePath"));
report.setTaskId(Rows.text(row, "taskId"));
report.setTaskResultId(Rows.text(row, "taskResultId"));
report.setTaskPatrolledId(Rows.text(row, "taskPatrolledId"));
report.setLocation(Rows.text(row, "location"));
report.setFilter(Rows.text(row, "filter"));
report.setSyncedAt(LocalDateTime.now());
report.setIsDeleted(0);
return report;
}
}

+ 120
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/PatrolTaskSyncService.java View File

@ -0,0 +1,120 @@
package com.inspect.agg.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.calendar.AggTaskCalendarService;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggPatrolTask;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggPatrolTaskMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 任务方案同步服务(源 patrol_task,接口 /patrol/task/list,量小,周期全量对账)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PatrolTaskSyncService {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggPatrolTaskMapper aggPatrolTaskMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
private final AggTaskCalendarService calendarService;
public void syncFullAllStations() {
stationRegistry.listEnabled().parallelStream().forEach(this::syncFull);
}
public void syncFull(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
int deletedMarked = 0;
String message = null;
try {
List<JsonNode> rows = stationApiClient.fetchAllRows(station, "patrol/task/list",
props.getSync().getBase().getPageSize(), 100, "taskId");
List<Long> ids = new ArrayList<>();
for (JsonNode row : rows) {
AggPatrolTask p = map(row, station);
if (p == null) {
continue;
}
aggPatrolTaskMapper.upsert(p);
if (p.getTaskId() != null) {
ids.add(p.getTaskId());
}
upserted++;
}
deletedMarked = aggPatrolTaskMapper.markDeletedExcept(station.getStationCode(), ids);
calendarService.evictStation(station.getStationCode());
message = "rows=" + rows.size();
log.info("station={} patrol_task FULL ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} patrol_task FULL failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "taskplan", "FULL", status, upserted, deletedMarked,
message, started, LocalDateTime.now());
}
private AggPatrolTask map(JsonNode r, AggStation station) {
Long taskId = Rows.longVal(r, "taskId");
if (taskId == null) {
return null;
}
AggPatrolTask p = new AggPatrolTask();
p.setStationCode(station.getStationCode());
p.setTaskId(taskId);
p.setTaskCode(Rows.text(r, "taskCode"));
p.setTaskName(Rows.text(r, "taskName"));
p.setType(Rows.text(r, "type"));
p.setPriority(Rows.text(r, "priority"));
p.setDeviceLevel(Rows.text(r, "deviceLevel"));
p.setDeviceList(Rows.text(r, "deviceList"));
p.setFixedStartTime(Rows.date(r, "fixedStartTime"));
p.setIsEnable(Rows.text(r, "isEnable", "isenable"));
p.setFileStatus(Rows.text(r, "fileStatus"));
p.setExecutionStatus(Rows.text(r, "executionStatus"));
p.setTaskCycle(Rows.text(r, "taskCycle"));
p.setCycleMonth(Rows.text(r, "cycleMonth"));
p.setCycleWeek(Rows.text(r, "cycleWeek"));
p.setCycleExecuteTime(Rows.text(r, "cycleExecuteTime"));
p.setCycleStartTime(Rows.date(r, "cycleStartTime"));
p.setCycleEndTime(Rows.date(r, "cycleEndTime"));
p.setIntervalNumber(Rows.intVal(r, "intervalNumber"));
p.setIntervalType(Rows.text(r, "intervalType"));
p.setIntervalExecuteTime(Rows.text(r, "intervalExecuteTime"));
p.setIntervalStartTime(Rows.date(r, "intervalStartTime"));
p.setIntervalEndTime(Rows.date(r, "intervalEndTime"));
p.setInvalidStartTime(Rows.date(r, "invalidStartTime"));
p.setInvalidEndTime(Rows.date(r, "invalidEndTime"));
p.setDevType(Rows.text(r, "devType"));
p.setAreaId(Rows.text(r, "areaId"));
p.setAreaName(Rows.text(r, "areaName"));
p.setPointType(Rows.text(r, "pointType"));
Integer totalPoints = Rows.intVal(r, "totalPoints");
p.setTotalPoints(totalPoints == null ? 0 : totalPoints);
p.setExecutionMode(Rows.text(r, "executionMode"));
p.setDrafter(Rows.text(r, "drafter"));
p.setSpecifiedAlg(Rows.text(r, "specifiedAlg"));
p.setCreateTime(Rows.date(r, "createTime"));
p.setSyncedAt(LocalDateTime.now());
p.setIsDeleted(0);
return p;
}
}

+ 99
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/PointSyncService.java View File

@ -0,0 +1,99 @@
package com.inspect.agg.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggBasedataPatrolpoint;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggBasedataPatrolpointMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 巡检点位同步服务(基础数据全量:源 basedata_patrolpoint)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PointSyncService {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggBasedataPatrolpointMapper aggPointMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
public void syncAllStations() {
for (AggStation station : stationRegistry.listEnabled()) {
syncStation(station);
}
}
public void syncStation(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
int deletedMarked = 0;
String message = null;
try {
List<JsonNode> rows = stationApiClient.fetchAllPatrolPoints(station,
props.getSync().getBase().getPageSize(), 500);
List<Long> ids = new ArrayList<>();
for (JsonNode row : rows) {
AggBasedataPatrolpoint p = map(row, station);
if (p == null) {
continue;
}
aggPointMapper.upsert(p);
if (p.getPatrolPointId() != null) {
ids.add(p.getPatrolPointId());
}
upserted++;
}
deletedMarked = aggPointMapper.markDeletedExcept(station.getStationCode(), ids);
message = "rows=" + rows.size();
log.info("station={} point sync ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} point sync failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "point", "FULL", status, upserted, deletedMarked,
message, started, LocalDateTime.now());
}
private AggBasedataPatrolpoint map(JsonNode r, AggStation station) {
Long pointId = Rows.longVal(r, "patrolpointId");
if (pointId == null) {
return null;
}
AggBasedataPatrolpoint p = new AggBasedataPatrolpoint();
p.setStationCode(station.getStationCode());
p.setPatrolPointId(pointId);
p.setPatrolPointCode(Rows.text(r, "patrolpointCode"));
p.setPatrolPointName(Rows.text(r, "patrolpointName"));
p.setMainDeviceId(Rows.longVal(r, "mainDeviceId"));
p.setDeviceId(Rows.longVal(r, "deviceId"));
p.setAreaId(Rows.longVal(r, "areaId"));
p.setAreaName(Rows.text(r, "areaName"));
p.setPatrolContent(Rows.text(r, "patrolContent"));
p.setPurpose(Rows.text(r, "purpose"));
p.setPatrolTypeId(Rows.text(r, "patrolTypeId"));
p.setPointImptLevel(Rows.text(r, "pointImptLevel"));
p.setAlarmThreshold(Rows.text(r, "alarmThreshold"));
p.setAlgSubtypeIds(Rows.text(r, "algSubtypeIds"));
p.setUpdateTime(Rows.date(r, "updateTime"));
p.setSyncedAt(LocalDateTime.now());
p.setIsDeleted(0);
return p;
}
}

+ 130
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/ResultAnalysisSyncService.java View File

@ -0,0 +1,130 @@
package com.inspect.agg.service;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggResultAnalysis;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggResultAnalysisMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import com.inspect.base.core.enums.DefectLevel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* result_analysis 汇聚同步服务。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ResultAnalysisSyncService {
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggResultAnalysisMapper resultAnalysisMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
public void syncLast12MonthsAllStations() {
stationRegistry.listEnabled().forEach(this::syncLast12Months);
}
public void syncRecentAllStations() {
LocalDateTime end = LocalDateTime.now();
LocalDateTime begin = end.minusDays(7);
stationRegistry.listEnabled().forEach(station -> syncRange(station, begin, end, "RECENT"));
}
public void syncLast12Months(AggStation station) {
LocalDateTime begin = YearMonth.now().minusMonths(11).atDay(1).atStartOfDay();
LocalDateTime end = YearMonth.now().plusMonths(1).atDay(1).atStartOfDay();
syncRange(station, begin, end, "FULL_12M");
}
private void syncRange(AggStation station, LocalDateTime begin, LocalDateTime end, String syncType) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
String message = null;
try {
Map<String, String> query = new HashMap<>();
query.put("beginTime", begin.format(DATE_TIME));
query.put("endTime", end.minusSeconds(1).format(DATE_TIME));
List<JsonNode> rows = stationApiClient.fetchAllRows(station, "patrol/analysis/agg/list",
props.getSync().getBase().getPageSize(), 1000, query, "lineId");
for (JsonNode row : rows) {
AggResultAnalysis item = map(row, station);
if (item == null) {
continue;
}
resultAnalysisMapper.upsert(item);
upserted++;
}
message = "rows=" + rows.size();
log.info("station={} result_analysis {} ok: upserted={}", station.getStationCode(), syncType, upserted);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} result_analysis {} failed: {}", station.getStationCode(), syncType, e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "analysis", syncType, status, upserted, 0,
message, started, LocalDateTime.now());
}
private AggResultAnalysis map(JsonNode row, AggStation station) {
Long lineId = Rows.longVal(row, "lineId");
if (lineId == null) {
return null;
}
String resultType = Rows.text(row, "resultType");
AggResultAnalysis item = new AggResultAnalysis();
item.setStationCode(station.getStationCode());
item.setLineId(lineId);
item.setResultType(resultType);
item.setBusinessId(Rows.text(row, "businessId"));
item.setResultContent(Rows.text(row, "resultContent"));
item.setResStatus(Rows.text(row, "resStatus"));
item.setPointStatus(Rows.text(row, "pointStatus"));
item.setSuggestion(Rows.text(row, "suggestion"));
item.setObjectId(Rows.text(row, "objectId"));
item.setDescription(Rows.text(row, "description"));
item.setRequestId(Rows.text(row, "requestId"));
item.setTaskPatrolId(Rows.text(row, "taskPatrolId"));
item.setAlgType(Rows.text(row, "algType"));
item.setResValue(Rows.text(row, "resValue"));
item.setResCode(Rows.text(row, "resCode"));
item.setResConf(Rows.text(row, "resConf"));
item.setResDesc(Rows.text(row, "resDesc"));
item.setResult(Rows.text(row, "result"));
item.setResImgUrl(Rows.text(row, "resImgUrl"));
item.setImageNormalUrlPath(Rows.text(row, "imageNormalUrlPath"));
item.setResPos(Rows.jsonText(row, "resPos"));
item.setFilter(Rows.text(row, "filter"));
item.setOriginalValue(Rows.text(row, "originalValue"));
Integer defectLevel = Rows.intVal(row, "defectLevel");
item.setDefectLevel(defectLevel == null ? deriveDefectLevel(resultType) : defectLevel);
item.setCreateTime(Rows.date(row, "createTime"));
item.setSyncedAt(LocalDateTime.now());
item.setIsDeleted(0);
return item;
}
private static int deriveDefectLevel(String resultType) {
if ("0".equals(resultType)) return DefectLevel.NORMAL.getCode();
if ("2".equals(resultType)) return DefectLevel.MINOR.getCode();
return DefectLevel.NONE.getCode();
}
}

+ 47
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/StationRegistry.java View File

@ -0,0 +1,47 @@
package com.inspect.agg.service;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggStationMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 站点注册表(DB 维护):查询走 Mapper XML
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StationRegistry {
private final AggStationMapper aggStationMapper;
/** 所有启用站点 */
public List<AggStation> listEnabled() {
List<AggStation> list = aggStationMapper.selectEnabled();
log.info("listEnabled: {}", list);
return list;
}
/** 全部站点(含停用) */
public List<AggStation> listAll() {
return aggStationMapper.selectAllOrdered();
}
/** 按编码查站点(不存在抛异常) */
public AggStation requireByCode(String stationCode) {
if (!StringUtils.hasText(stationCode)) {
throw new IllegalArgumentException("stationCode 不能为空");
}
AggStation station = aggStationMapper.selectByCode(stationCode);
if (station == null) {
throw new IllegalArgumentException("站点不存在: " + stationCode);
}
return station;
}
}

+ 147
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/TaskExecSyncService.java View File

@ -0,0 +1,147 @@
package com.inspect.agg.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggPatrolTaskStatus;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggPatrolTaskStatusMapper;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.support.Rows;
import com.inspect.agg.support.SyncLogWriter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 任务执行同步服务(动态数据)。
* 站点侧 patrol_task_status 无 update_time,无法时间水位:
* - ACTIVE:高频拉 taskState=2(执行中)子集,满足 1 分钟新鲜度
* - FULL:周期全量 + 对账删除,覆盖终态/状态变化
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TaskExecSyncService {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final AggPatrolTaskStatusMapper aggTaskExecMapper;
private final SyncLogWriter syncLogWriter;
private final AggProperties props;
public void syncActiveAllStations() {
stationRegistry.listEnabled().parallelStream().forEach(this::syncActive);
}
public void syncFullAllStations() {
stationRegistry.listEnabled().parallelStream().forEach(this::syncFull);
}
/** 高频:只拉执行中任务 */
public void syncActive(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
String message = null;
try {
List<JsonNode> rows = stationApiClient.fetchAllRows(station, "patrol/taskstatus/list",
props.getSync().getBase().getPageSize(), 50,
Collections.singletonMap("taskState", "2"), "lineId");
for (JsonNode row : rows) {
if (upsert(row, station)) {
upserted++;
}
}
message = "rows=" + rows.size();
log.info("station={} task ACTIVE ok: upserted={}", station.getStationCode(), upserted);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} task ACTIVE failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "task", "ACTIVE", status, upserted, 0,
message, started, LocalDateTime.now());
}
/** 周期:全量拉取 + 对账删除 */
public void syncFull(AggStation station) {
LocalDateTime started = LocalDateTime.now();
String status = "SUCCESS";
int upserted = 0;
int deletedMarked = 0;
String message = null;
try {
List<JsonNode> rows = stationApiClient.fetchAllRows(station, "patrol/taskstatus/list",
500, 100, "lineId");
List<Long> ids = new ArrayList<>();
for (JsonNode row : rows) {
if (upsert(row, station) && row.hasNonNull("lineId")) {
ids.add(row.get("lineId").asLong());
}
}
deletedMarked = aggTaskExecMapper.markDeletedExcept(station.getStationCode(), ids);
upserted = ids.size();
message = "rows=" + rows.size();
log.info("station={} task FULL ok: upserted={}, deletedMarked={}", station.getStationCode(), upserted, deletedMarked);
} catch (Exception e) {
status = "FAILED";
message = Rows.abbrev(e.getMessage(), 480);
log.error("station={} task FULL failed: {}", station.getStationCode(), e.getMessage());
}
syncLogWriter.write(station.getStationCode(), "task", "FULL", status, upserted, deletedMarked,
message, started, LocalDateTime.now());
}
private boolean upsert(JsonNode r, AggStation station) {
AggPatrolTaskStatus e = map(r, station);
if (e == null) {
return false;
}
aggTaskExecMapper.upsert(e);
return true;
}
private AggPatrolTaskStatus map(JsonNode r, AggStation station) {
Long lineId = Rows.longVal(r, "lineId");
if (lineId == null) {
return null;
}
AggPatrolTaskStatus e = new AggPatrolTaskStatus();
e.setStationCode(station.getStationCode());
e.setLineId(lineId);
e.setTaskPatrolledId(Rows.text(r, "taskPatrolledId"));
e.setTaskName(Rows.text(r, "taskName"));
e.setTaskCode(Rows.text(r, "taskCode"));
e.setTaskState(Rows.text(r, "taskState"));
e.setPlanStartTime(Rows.date(r, "planStartTime"));
e.setStartTime(Rows.date(r, "startTime"));
e.setEndTime(Rows.date(r, "endTime"));
e.setTaskProgress(Rows.text(r, "taskProgress"));
e.setTaskEstimatedTime(Rows.text(r, "taskEstimatedTime"));
e.setPatrolType(Rows.text(r, "patrolType"));
e.setPosType(Rows.text(r, "posType"));
e.setCode(Rows.text(r, "code"));
e.setPos(Rows.text(r, "pos"));
e.setExecType(Rows.text(r, "execType"));
e.setTaskType(Rows.text(r, "taskType"));
e.setMainId(Rows.text(r, "mainId"));
Integer qxNum = Rows.intVal(r, "qxNum");
e.setQxNum(qxNum == null ? 0 : qxNum);
String fileStatus = Rows.text(r, "fileStatus");
e.setFileStatus(fileStatus == null ? "0" : fileStatus);
e.setSummary(Rows.jsonText(r, "summary"));
e.setDescription(Rows.text(r, "description"));
e.setCreateTime(Rows.date(r, "createTime"));
e.setSyncedAt(LocalDateTime.now());
e.setIsDeleted(0);
return e;
}
}

+ 386
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/station/StationApiClient.java View File

@ -0,0 +1,386 @@
package com.inspect.agg.station;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.inspect.agg.entity.AggStation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 站点 API 客户端:getToken 鉴权(Bearer)+ 基础数据分页拉取。
* 兼容两种响应形态:{data:[...]} 与 RuoYi 分页 {total,rows}(根级或 data 内)。
* 每个实体的行唯一键由调用方显式指定(idKeys),禁止回退到可重复字段判重。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class StationApiClient {
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
/** 每站点 token 缓存:value + 过期时刻(epoch ms),提前 60s 续期 */
private final Map<String, Token> tokenCache = new ConcurrentHashMap<>();
/** 令牌缓存项(JDK8 兼容写法,替代 record) */
private static final class Token {
private final String value;
private final long expireAtMs;
Token(String value, long expireAtMs) {
this.value = value;
this.expireAtMs = expireAtMs;
}
String value() {
return value;
}
boolean valid() {
return value != null && System.currentTimeMillis() < expireAtMs - 60_000L;
}
}
public List<JsonNode> fetchAllAreas(AggStation station, int pageSize, int maxPages) {
return fetchAllRows(station, "basedata/area/list", pageSize, maxPages, "areaId");
}
public List<JsonNode> fetchAllInspectionReports(AggStation station, int pageSize, int maxPages) {
return fetchAllRows(station, "patrol/insreport/list", pageSize, maxPages, "lineId");
}
public List<JsonNode> fetchAllDevices(AggStation station, int pageSize, int maxPages) {
return fetchAllRows(station, "basedata/prodevmnt/device/list", pageSize, maxPages, "deviceId");
}
public List<JsonNode> fetchAllPatrolPoints(AggStation station, int pageSize, int maxPages) {
return fetchAllRows(station, "basedata/patrolpointmnt/patrolpoint/list", pageSize, maxPages, "patrolpointId");
}
public List<JsonNode> fetchAllEqpbooks(AggStation station, int pageSize, int maxPages) {
return fetchAllRows(station, "basedata/eqpbook/list", pageSize, maxPages, "eqpbookId");
}
public List<JsonNode> fetchAllChannels(AggStation station, int pageSize, int maxPages) {
return fetchAllRows(station, "basedata/eqpbook/channel/list", pageSize, maxPages, "channelId");
}
public List<JsonNode> fetchAllDeviceTypes(AggStation station, int pageSize, int maxPages) {
return fetchAllRows(station, "basedata/devicetype/list", pageSize, maxPages, "devicetypeId");
}
public JsonNode fetchAggDefectMonth(AggStation station, String beginMonth, String endMonth) {
String url = UriComponentsBuilder.fromHttpUrl(stripTrailingSlash(station.getBaseUrl()) + "/patrol/analysis/agg/defect/month")
.queryParam("beginMonth", beginMonth)
.queryParam("endMonth", endMonth)
.toUriString();
return doGetJson(url, station);
}
/**
* 取摄像机 RTSP 地址(站端 inspect-ivs 接口):
* GET {base_url}/ivs/api/v1/stream/rtspUrl?cameraCode=xxx
*/
public JsonNode fetchRtspUrl(AggStation station, String cameraCode, Integer streamType) {
String url = UriComponentsBuilder.fromHttpUrl(stripTrailingSlash(station.getBaseUrl()) + "/ivs/api/v1/stream/rtspUrl")
.queryParam("cameraCode", cameraCode)
.queryParam("streamType", streamType)
.toUriString();
return getJson(url, ensureToken(station));
}
/** 启动站端转流:GET {base_url}/ivs/api/v1/stream/start?cameraCode=xxx(返回 StreamID/FLV/HLS 等) */
public JsonNode fetchStreamStart(AggStation station, String cameraCode, Integer streamType) {
String url = UriComponentsBuilder.fromHttpUrl(stripTrailingSlash(station.getBaseUrl()) + "/ivs/api/v1/stream/start")
.queryParam("cameraCode", cameraCode)
.queryParam("streamType", streamType)
.toUriString();
return getJson(url, ensureToken(station));
}
public JsonNode fetchRealtimeStart(AggStation station, String cameraCode, Integer streamType) {
String url = stripTrailingSlash(station.getBaseUrl()) + "/patrol/video/realtime/start";
Map<String, Object> body = new LinkedHashMap<>();
body.put("streamType", streamType == null ? 2 : streamType);
body.put("cameraCode", cameraCode);
return doPostJson(url, body, station);
}
/** 停止站端实时转流:POST {base_url}/patrol/video/realtime/stop */
public JsonNode fetchRealtimeStop(AggStation station, String stationStreamId,
String cameraCode, Integer streamType) {
String url = stripTrailingSlash(station.getBaseUrl()) + "/patrol/video/realtime/stop";
Map<String, Object> body = new LinkedHashMap<>();
if (stationStreamId != null && !stationStreamId.trim().isEmpty()) {
body.put("streamId", stationStreamId);
}
body.put("streamType", streamType == null ? 2 : streamType);
body.put("cameraCode", cameraCode);
return doPostJson(url, body, station);
}
/** 停止站端转流:GET {base_url}/ivs/api/v1/stream/stop?streamId=xxx */
public JsonNode fetchStreamStop(AggStation station, String streamId) {
String url = UriComponentsBuilder.fromHttpUrl(stripTrailingSlash(station.getBaseUrl()) + "/ivs/api/v1/stream/stop")
.queryParam("streamId", streamId)
.toUriString();
return getJson(url, ensureToken(station));
}
/** 创建站端 HLS 历史回放会话。 */
public JsonNode createHlsPlaybackSession(AggStation station, String cameraCode, Integer streamType,
String startTime, String endTime) {
String url = stripTrailingSlash(station.getBaseUrl()) + "/patrol/videotape/hls/ui/session/create";
Map<String, Object> body = new LinkedHashMap<>();
body.put("streamType", streamType);
body.put("cameraCode", cameraCode);
body.put("startTime", startTime);
body.put("endTime", endTime);
return doPostJson(url, body, station);
}
/** 控制站端 HLS 历史回放会话。 */
public JsonNode controlHlsPlaybackSession(AggStation station, String sessionId, String cameraCode, String action) {
String url = stripTrailingSlash(station.getBaseUrl())
+ "/patrol/videotape/hls/ui/session/control/" + sessionId;
Map<String, Object> body = new LinkedHashMap<>();
body.put("cameraCode", cameraCode);
body.put("action", action);
return doPostJson(url, body, station);
}
/** 通用分页拉取:按 total 或页大小/重复检测终止 */
public List<JsonNode> fetchAllRows(AggStation station, String path, int pageSize, int maxPages, String... idKeys) {
return fetchAllRows(station, path, pageSize, maxPages, Collections.emptyMap(), idKeys);
}
/** 带附加查询参数(如 taskState/fileStatus)的分页拉取 */
public List<JsonNode> fetchAllRows(AggStation station, String path, int pageSize, int maxPages,
Map<String, String> extraQuery, String... idKeys) {
String token = ensureToken(station);
List<JsonNode> rows = new ArrayList<>();
Set<String> seenKeys = new HashSet<>();
int page = 1;
long total = -1;
boolean duplicateBreak = false;
while (page <= maxPages) {
UriComponentsBuilder ub = UriComponentsBuilder.fromHttpUrl(stripTrailingSlash(station.getBaseUrl()) + "/" + path)
.queryParam("pageNum", page)
.queryParam("pageSize", pageSize);
if (extraQuery != null) {
extraQuery.forEach(ub::queryParam);
}
String url = ub.toUriString();
PageData pd = doGetRows(url, token, station);
int before = rows.size();
for (JsonNode r : pd.rows()) {
String key = rowKey(r, idKeys);
if (key != null && !seenKeys.add(key)) {
duplicateBreak = true; // 接口未按分页生效,返回重复全集
continue;
}
rows.add(r);
}
if (pd.total() >= 0) {
if (rows.size() >= pd.total()) {
break;
}
} else {
if (pd.rows().isEmpty() || (rows.size() - before) == 0) {
break;
}
}
if (duplicateBreak) {
break;
}
if (pd.total() < 0 && pd.rows().size() < pageSize) {
break;
}
page++;
}
log.info("station={} fetch {} done: rows={} (pages tried={})", station.getStationCode(), path, rows.size(), page);
return rows;
}
/** 获取有效 token(缓存 + 惰性刷新) */
public String ensureToken(AggStation station) {
Token cached = tokenCache.get(station.getStationCode());
if (cached != null && cached.valid()) {
return cached.value;
}
synchronized (tokenCache) {
cached = tokenCache.get(station.getStationCode());
if (cached != null && cached.valid()) {
return cached.value;
}
String token = requestToken(station);
tokenCache.put(station.getStationCode(), new Token(token, System.currentTimeMillis() + 525_600_000L));
return token;
}
}
private String requestToken(AggStation station) {
String url = UriComponentsBuilder.fromHttpUrl(stripTrailingSlash(station.getBaseUrl()) + "/auth/getToken")
.queryParam("username", station.getUsername())
.queryParam("password", station.getPassword())
.toUriString();
JsonNode root = getJson(url, null);
if (root.path("code").asInt() != 200) {
throw new IllegalStateException("getToken 失败: " + root.path("data").asText(root.path("msg").asText()));
}
JsonNode data = root.path("data");
String token = data.path("access_token").asText(null);
if (token == null || token.trim().isEmpty()) {
throw new IllegalStateException("getToken 响应缺少 access_token: " + root);
}
return token;
}
private PageData doGetRows(String url, String token, AggStation station) {
try {
return parseRows(getJson(url, token));
} catch (RuntimeException e) {
log.warn("station={} first attempt failed: {}", station.getStationCode(), e.getMessage());
String fresh = requestToken(station);
tokenCache.put(station.getStationCode(), new Token(fresh, System.currentTimeMillis() + 525_600_000L));
return parseRows(getJson(url, fresh));
}
}
/** 兼容多种响应形态:{data:[...]} / {data:{rows,total}} / {rows,total}(根级) */
private PageData parseRows(JsonNode root) {
if (root.path("code").asInt() != 200) {
throw new IllegalStateException("接口返回错误 code=" + root.path("code").asText()
+ " msg=" + root.path("msg").asText() + " data=" + root.path("data").asText());
}
JsonNode node = root.has("data") ? root.get("data") : root;
long total = -1;
JsonNode arr = null;
if (node.isArray()) {
arr = node;
} else if (node != null && node.isObject()) {
if (node.has("rows")) {
arr = node.get("rows");
total = node.path("total").asLong(-1);
} else if (node.has("row")) {
arr = node.get("row");
total = node.path("total").asLong(-1);
} else if (node.has("list")) {
arr = node.get("list");
total = node.path("total").asLong(-1);
}
}
if (arr == null || !arr.isArray()) {
throw new IllegalStateException("无法识别的响应结构,请人工核对接口返回: " + root);
}
List<JsonNode> rows = new ArrayList<>();
arr.forEach(rows::add);
return new PageData(rows, total);
}
private JsonNode doGetJson(String url, AggStation station) {
try {
return getJson(url, ensureToken(station));
} catch (RuntimeException e) {
log.warn("station={} GET first attempt failed: {}", station.getStationCode(), e.getMessage());
String fresh = requestToken(station);
tokenCache.put(station.getStationCode(), new Token(fresh, System.currentTimeMillis() + 525_600_000L));
return getJson(url, fresh);
}
}
private JsonNode doPostJson(String url, Object body, AggStation station) {
try {
return postJson(url, body, ensureToken(station));
} catch (RuntimeException e) {
log.warn("station={} POST first attempt failed: {}", station.getStationCode(), e.getMessage());
String fresh = requestToken(station);
tokenCache.put(station.getStationCode(), new Token(fresh, System.currentTimeMillis() + 525_600_000L));
return postJson(url, body, fresh);
}
}
private JsonNode postJson(String url, Object body, String token) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
if (token != null) {
headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token);
}
ResponseEntity<String> resp = restTemplate.exchange(URI.create(url), HttpMethod.POST, new HttpEntity<>(body, headers), String.class);
try {
return objectMapper.readTree(resp.getBody());
} catch (Exception e) {
throw new IllegalStateException("响应 JSON 解析失败: " + e.getMessage());
}
}
private JsonNode getJson(String url, String token) {
HttpHeaders headers = new HttpHeaders();
if (token != null) {
headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token);
}
ResponseEntity<String> resp = restTemplate.exchange(URI.create(url), HttpMethod.GET, new HttpEntity<>(headers), String.class);
try {
return objectMapper.readTree(resp.getBody());
} catch (Exception e) {
throw new IllegalStateException("响应 JSON 解析失败: " + e.getMessage());
}
}
/** 行唯一键:只取调用方指定的实体主键字段;均缺失时用 id */
private static String rowKey(JsonNode r, String... idKeys) {
if (idKeys != null) {
for (String k : idKeys) {
if (r.has(k) && !r.get(k).isNull()) {
return r.get(k).asText();
}
}
}
if (r.has("id") && !r.get("id").isNull()) {
return r.get("id").asText();
}
return null;
}
private static String stripTrailingSlash(String base) {
return base == null ? "" : base.replaceAll("/+$", "");
}
/** 分页结果(JDK8 兼容写法,替代 record) */
private static final class PageData {
private final List<JsonNode> rows;
private final long total;
PageData(List<JsonNode> rows, long total) {
this.rows = rows;
this.total = total;
}
List<JsonNode> rows() {
return rows;
}
long total() {
return total;
}
}
}

+ 23
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/station/TokenData.java View File

@ -0,0 +1,23 @@
package com.inspect.agg.station;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* getToken 接口响应 data 部分。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class TokenData {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("token_type")
private String tokenType;
@JsonProperty("expires_in")
private Long expiresIn;
}

+ 103
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/support/Rows.java View File

@ -0,0 +1,103 @@
package com.inspect.agg.support;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import com.fasterxml.jackson.databind.JsonNode;
/**
* 站点响应行 JsonNode 字段读取工具(兼容 camelCase/snake_case、字符串数字)。
*/
public final class Rows {
private static final DateTimeFormatter SRC_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter SRC_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private Rows() {
}
public static String text(JsonNode n, String... keys) {
for (String k : keys) {
JsonNode v = n.get(k);
if (v != null && !v.isNull() && v.isTextual()) {
return v.asText();
}
}
return null;
}
public static Long longVal(JsonNode n, String... keys) {
for (String k : keys) {
JsonNode v = n.get(k);
if (v != null && !v.isNull() && v.isNumber()) {
return v.asLong();
}
}
return null;
}
public static Integer intVal(JsonNode n, String... keys) {
for (String k : keys) {
JsonNode v = n.get(k);
if (v == null || v.isNull()) {
continue;
}
if (v.isNumber()) {
return v.asInt();
}
if (v.isTextual()) {
try {
return Integer.parseInt(v.asText().trim());
} catch (NumberFormatException ignore) {
// 非数字字符串按空处理
}
}
}
return null;
}
public static LocalDateTime date(JsonNode n, String... keys) {
for (String k : keys) {
JsonNode v = n.get(k);
if (v != null && !v.isNull() && v.isTextual()) {
String s = v.asText().trim();
if (!s.isEmpty()) {
try {
return LocalDateTime.parse(s, SRC_TIME);
} catch (Exception ignore) {
// ignore
}
try {
return LocalDate.parse(s, SRC_DATE).atStartOfDay();
} catch (Exception ignore) {
// 非标准格式按 null 处理
}
}
}
}
return null;
}
/** 文本优先,对象/数组转 JSON 字符串(如 summary) */
public static String jsonText(JsonNode n, String... keys) {
for (String k : keys) {
JsonNode v = n.get(k);
if (v == null || v.isNull()) {
continue;
}
if (v.isTextual()) {
return v.asText();
}
return v.toString();
}
return null;
}
public static String abbrev(String s, int max) {
if (s == null) {
return null;
}
return s.length() <= max ? s : s.substring(0, max);
}
}

+ 42
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/support/SyncLogWriter.java View File

@ -0,0 +1,42 @@
package com.inspect.agg.support;
import java.time.LocalDateTime;
import org.springframework.stereotype.Component;
import com.inspect.agg.entity.AggSyncLog;
import com.inspect.agg.mapper.AggSyncLogMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 同步运行日志写入(原生 MyBatis,XML 实现)。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SyncLogWriter {
private final AggSyncLogMapper aggSyncLogMapper;
public void write(String stationCode, String entity, String syncType, String status,
int rowCount, int deletedCount, String message,
LocalDateTime started, LocalDateTime finished) {
try {
AggSyncLog log = new AggSyncLog();
log.setStationCode(stationCode);
log.setEntity(entity);
log.setSyncType(syncType);
log.setStatus(status);
log.setRowCount(rowCount);
log.setDeletedCount(deletedCount);
log.setMessage(message);
log.setStartedAt(started);
log.setFinishedAt(finished);
aggSyncLogMapper.insert(log);
} catch (Exception e) {
log.error("write agg_sync_log failed: {}", e.getMessage());
}
}
}

+ 16
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/PlaybackSession.java View File

@ -0,0 +1,16 @@
package com.inspect.agg.video;
import lombok.Data;
/**
* 汇聚侧历史回放会话。
*/
@Data
public class PlaybackSession {
private String stationCode;
private String cameraCode;
private String sessionId;
private String streamId;
private long createdAt = System.currentTimeMillis();
}

+ 55
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/PlaybackSessionRegistry.java View File

@ -0,0 +1,55 @@
package com.inspect.agg.video;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 历史回放会话注册表。
*/
@Component
public class PlaybackSessionRegistry {
private final Map<String, List<PlaybackSession>> sessions = new ConcurrentHashMap<>();
public void add(String stationCode, String cameraCode, String sessionId, String streamId) {
PlaybackSession session = new PlaybackSession();
session.setStationCode(stationCode);
session.setCameraCode(cameraCode);
session.setSessionId(sessionId);
session.setStreamId(streamId);
sessions.compute(key(stationCode, cameraCode), (ignored, current) -> {
List<PlaybackSession> list = current == null ? new ArrayList<>() : current;
list.removeIf(item -> sessionId.equals(item.getSessionId()));
list.add(0, session);
return list;
});
}
public List<PlaybackSession> list(String stationCode, String cameraCode) {
List<PlaybackSession> current = sessions.get(key(stationCode, cameraCode));
if (current == null || current.isEmpty()) {
return Collections.emptyList();
}
return new ArrayList<>(current);
}
public void remove(String stationCode, String cameraCode, String sessionId) {
sessions.computeIfPresent(key(stationCode, cameraCode), (ignored, current) -> {
current.removeIf(item -> sessionId.equals(item.getSessionId()));
return current.isEmpty() ? null : current;
});
}
private static String key(String stationCode, String cameraCode) {
String base = StringUtils.hasText(cameraCode) && cameraCode.contains("#")
? cameraCode.substring(0, cameraCode.indexOf('#'))
: cameraCode;
return stationCode + "|" + base;
}
}

+ 33
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamBinding.java View File

@ -0,0 +1,33 @@
package com.inspect.agg.video;
import lombok.Data;
/**
* 汇聚实时流与站端实时流之间的绑定关系。
*/
@Data
public class RealtimeStreamBinding {
/** 站点编码。 */
private String stationCode;
/** 原始摄像机编码,保留 cameraCode 中 # 后的附加信息。 */
private String cameraCode;
/** 汇聚 ZLMediaKit 中使用的确定性流 ID。 */
private String aggStreamId;
/** 站端实时流 ID;直连 RTSP 模式为空。 */
private String stationStreamId;
/** 汇聚 ZLMediaKit 拉取的源 RTSP 地址。 */
private String sourceRtsp;
/** 流类型,透传给站端 stop 接口。 */
private Integer streamType;
/** 是否由站端启动和停止;false 表示调用方直接传入 RTSP。 */
private boolean stationManaged = true;
private long createdAt = System.currentTimeMillis();
}

+ 65
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamRegistry.java View File

@ -0,0 +1,65 @@
package com.inspect.agg.video;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 实时流绑定注册表:按站点 + 摄像机维护汇聚流与站端流的关系。
*/
@Component
public class RealtimeStreamRegistry {
private final Map<String, RealtimeStreamBinding> byCamera = new ConcurrentHashMap<>();
private final Map<String, String> aggStreamIdToCameraKey = new ConcurrentHashMap<>();
public static String cameraKey(String stationCode, String cameraCode) {
String base = cameraCode;
if (base != null && base.contains("#")) {
base = base.substring(0, base.indexOf('#'));
}
return stationCode + "|" + base;
}
public void put(RealtimeStreamBinding binding) {
if (binding == null
|| !StringUtils.hasText(binding.getStationCode())
|| !StringUtils.hasText(binding.getCameraCode())
|| !StringUtils.hasText(binding.getAggStreamId())) {
throw new IllegalArgumentException("实时流绑定信息不完整");
}
String key = cameraKey(binding.getStationCode(), binding.getCameraCode());
byCamera.put(key, binding);
aggStreamIdToCameraKey.put(binding.getAggStreamId(), key);
}
public RealtimeStreamBinding getByCamera(String stationCode, String cameraCode) {
if (!StringUtils.hasText(stationCode) || !StringUtils.hasText(cameraCode)) {
return null;
}
return byCamera.get(cameraKey(stationCode, cameraCode));
}
public RealtimeStreamBinding getByAggStreamId(String aggStreamId) {
String cameraKey = aggStreamIdToCameraKey.get(aggStreamId);
return cameraKey == null ? null : byCamera.get(cameraKey);
}
public RealtimeStreamBinding removeByAggStreamId(String aggStreamId) {
String cameraKey = aggStreamIdToCameraKey.remove(aggStreamId);
if (cameraKey == null) {
return null;
}
RealtimeStreamBinding binding = byCamera.remove(cameraKey);
if (binding == null) {
return null;
}
if (!aggStreamId.equals(binding.getAggStreamId())) {
// 正常不会发生;若同摄像机绑定已被替换,恢复反向索引,避免误删新绑定。
aggStreamIdToCameraKey.put(binding.getAggStreamId(), cameraKey);
}
return binding;
}
}

+ 340
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamService.java View File

@ -0,0 +1,340 @@
package com.inspect.agg.video;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.dto.VideoRequest;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.service.StationRegistry;
import com.inspect.agg.station.StationApiClient;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 实时流生命周期服务:统一处理站端启动、汇聚代理、观看引用计数和停止清理。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RealtimeStreamService {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final ZlmApiClient zlmApiClient;
private final RealtimeViewerRegistry viewerRegistry;
private final RealtimeStreamRegistry streamRegistry;
/** 单实例部署下按相机串行化 start/stop,避免并发重复创建代理。 */
private final Map<String, Object> cameraLocks = new ConcurrentHashMap<>();
public StartResult start(String stationCode, VideoRequest request) {
if (request == null
|| !StringUtils.hasText(stationCode)
|| !StringUtils.hasText(request.getCameraCode())) {
throw new IllegalArgumentException("stationCode / cameraCode 不能为空");
}
String cameraCode = request.getCameraCode();
Integer streamType = request.getStreamType() == null ? 2 : request.getStreamType();
String directRtsp = request.getRtspUrl();
AggStation station = stationRegistry.requireByCode(stationCode);
String aggStreamId = aggStreamId(stationCode, cameraCode);
synchronized (cameraLock(stationCode, cameraCode)) {
boolean mediaActive = zlmApiClient.streamExists(aggStreamId);
JsonNode existingProxy = zlmApiClient.findStreamProxy(aggStreamId);
boolean proxyActive = existingProxy != null;
RealtimeStreamBinding binding = streamRegistry.getByCamera(stationCode, cameraCode);
boolean stationStartAttempted = false;
try {
if (!mediaActive && !proxyActive) {
// 既无在线媒体也无拉流代理:调站端启动并新建汇聚代理。
if (StringUtils.hasText(directRtsp)) {
binding = directBinding(stationCode, cameraCode, aggStreamId, directRtsp, streamType);
} else {
binding = startStationStream(station, cameraCode, aggStreamId, streamType);
stationStartAttempted = true;
}
// addStreamProxy 已对 already exists 幂等处理,兜底并发/残留同名代理。
zlmApiClient.addStreamProxy(aggStreamId, binding.getSourceRtsp());
} else if (binding == null) {
// ZLM 已有在线媒体或拉流代理,但本机绑定丢失(例如服务重启后)。
// 不再重复调用站端 start(避免站端重复生成一路流),改用代理源地址补登绑定。
if (StringUtils.hasText(directRtsp)) {
binding = directBinding(stationCode, cameraCode, aggStreamId, directRtsp, streamType);
} else {
String sourceRtsp = existingProxy == null ? null : existingProxy.path("url").asText(null);
binding = recoveredBinding(stationCode, cameraCode, aggStreamId, streamType, sourceRtsp);
}
} else {
binding.setAggStreamId(aggStreamId);
}
streamRegistry.put(binding);
viewerRegistry.add(aggStreamId);
// 媒体已注册可播放返回 PLAYING;代理已存在但尚未出流返回 STARTING。
return new StartResult(aggStreamId, mediaActive);
} catch (RuntimeException e) {
if (stationStartAttempted) {
stopStationQuietly(station, binding, cameraCode, streamType, null);
}
throw e;
}
}
}
/**
* 停止一个观看者。
*
* @return -1 没有观看票据;0 最后一个观看者已退出并停止流;正数表示剩余观看者数量
*/
public int stop(String stationCode, VideoRequest request) {
if (request == null
|| !StringUtils.hasText(stationCode)
|| !StringUtils.hasText(request.getCameraCode())) {
throw new IllegalArgumentException("stationCode / cameraCode 不能为空");
}
String cameraCode = request.getCameraCode();
Integer streamType = request.getStreamType() == null ? 2 : request.getStreamType();
AggStation station = stationRegistry.requireByCode(stationCode);
String aggStreamId = aggStreamId(stationCode, cameraCode);
synchronized (cameraLock(stationCode, cameraCode)) {
int remaining = viewerRegistry.remove(aggStreamId);
if (remaining != 0) {
return remaining;
}
boolean deleted;
try {
deleted = zlmApiClient.delStreamProxy(aggStreamId);
} catch (RuntimeException e) {
viewerRegistry.add(aggStreamId);
throw e;
}
if (!deleted) {
boolean stillExists;
try {
stillExists = zlmApiClient.streamExists(aggStreamId);
} catch (RuntimeException e) {
viewerRegistry.add(aggStreamId);
throw e;
}
if (stillExists) {
viewerRegistry.add(aggStreamId);
throw new IllegalStateException("汇聚 ZLMediaKit 流仍存在,停止失败");
}
}
RealtimeStreamBinding binding = streamRegistry.removeByAggStreamId(aggStreamId);
viewerRegistry.clear(aggStreamId);
stopStationQuietly(station, binding, cameraCode, streamType, request.getRtspUrl());
return 0;
}
}
/** 定时清理过期观看票据;仅清理没有活跃观看者的汇聚流,并尽力停止对应站端流。 */
public void cleanupExpired() {
long now = System.currentTimeMillis();
List<String> expiredStreams = viewerRegistry.expire(now);
for (String aggStreamId : expiredStreams) {
RealtimeStreamBinding existing = streamRegistry.getByAggStreamId(aggStreamId);
String lockKey = existing == null
? "expired:" + aggStreamId
: RealtimeStreamRegistry.cameraKey(existing.getStationCode(), existing.getCameraCode());
synchronized (cameraLocks.computeIfAbsent(lockKey, ignored -> new Object())) {
if (viewerRegistry.count(aggStreamId) > 0) {
continue;
}
boolean deleted;
try {
deleted = zlmApiClient.delStreamProxy(aggStreamId);
} catch (Exception e) {
log.warn("delete expired agg stream failed, stream={} err={}", aggStreamId, e.getMessage());
continue;
}
if (!deleted) {
try {
if (zlmApiClient.streamExists(aggStreamId)) {
continue;
}
} catch (Exception e) {
log.warn("check expired agg stream failed, stream={} err={}", aggStreamId, e.getMessage());
continue;
}
}
RealtimeStreamBinding binding = streamRegistry.removeByAggStreamId(aggStreamId);
viewerRegistry.clear(aggStreamId);
RealtimeStreamBinding target = binding != null ? binding : existing;
if (target == null) {
continue;
}
try {
AggStation station = stationRegistry.requireByCode(target.getStationCode());
stopStationQuietly(station, target, target.getCameraCode(), target.getStreamType(), null);
log.info("realtime viewer tickets expired, close stream={}", aggStreamId);
} catch (Exception e) {
log.warn("stop expired station stream failed, stream={} err={}", aggStreamId, e.getMessage());
}
}
}
}
private RealtimeStreamBinding startStationStream(AggStation station, String cameraCode,
String aggStreamId, Integer streamType) {
JsonNode root = stationApiClient.fetchRealtimeStart(station, cameraCode, streamType);
JsonNode payload = successPayload(root);
String stationStreamId = textAnyDeep(payload, "streamId", "StreamID", "streamID", "id");
String sourceRtsp = textAnyDeep(payload, "RTSP", "rtspURL", "rtspUrl", "rtsp");
if (!StringUtils.hasText(sourceRtsp)) {
String stationIp = extractIp(station.getBaseUrl());
if (!StringUtils.hasText(stationStreamId) || !StringUtils.hasText(stationIp)) {
throw new IllegalStateException("站端未返回有效 streamId / RTSP 地址");
}
sourceRtsp = String.format("rtsp://%s:8554/live/%s", stationIp, stationStreamId);
}
RealtimeStreamBinding binding = new RealtimeStreamBinding();
binding.setStationCode(station.getStationCode());
binding.setCameraCode(cameraCode);
binding.setAggStreamId(aggStreamId);
binding.setStationStreamId(stationStreamId);
binding.setSourceRtsp(sourceRtsp);
binding.setStreamType(streamType);
binding.setStationManaged(true);
return binding;
}
private RealtimeStreamBinding directBinding(String stationCode, String cameraCode,
String aggStreamId, String rtspUrl, Integer streamType) {
RealtimeStreamBinding binding = new RealtimeStreamBinding();
binding.setStationCode(stationCode);
binding.setCameraCode(cameraCode);
binding.setAggStreamId(aggStreamId);
binding.setSourceRtsp(rtspUrl);
binding.setStreamType(streamType);
binding.setStationManaged(false);
return binding;
}
/**
* 服务重启后依据 ZLM 已有代理恢复内存绑定。
* 此时不掌握站端 streamId,stop 时仅靠 cameraCode 尽力通知站端停止。
*/
private RealtimeStreamBinding recoveredBinding(String stationCode, String cameraCode,
String aggStreamId, Integer streamType, String sourceRtsp) {
RealtimeStreamBinding binding = new RealtimeStreamBinding();
binding.setStationCode(stationCode);
binding.setCameraCode(cameraCode);
binding.setAggStreamId(aggStreamId);
binding.setSourceRtsp(sourceRtsp);
binding.setStreamType(streamType);
binding.setStationManaged(true);
return binding;
}
private void stopStationQuietly(AggStation station, RealtimeStreamBinding binding,
String fallbackCameraCode, Integer fallbackStreamType,
String directRtsp) {
if (binding != null && !binding.isStationManaged()) {
return;
}
if (binding == null && StringUtils.hasText(directRtsp)) {
return;
}
String cameraCode = binding != null && StringUtils.hasText(binding.getCameraCode())
? binding.getCameraCode() : fallbackCameraCode;
String stationStreamId = binding == null ? null : binding.getStationStreamId();
Integer streamType = binding != null && binding.getStreamType() != null
? binding.getStreamType() : fallbackStreamType;
if (!StringUtils.hasText(cameraCode)) {
return;
}
try {
JsonNode root = stationApiClient.fetchRealtimeStop(station, stationStreamId, cameraCode, streamType);
if (root.has("code") && root.path("code").asInt(-1) != 200) {
log.warn("stop station realtime stream returned non-success, station={} camera={} resp={}",
station.getStationCode(), cameraCode, root);
}
} catch (Exception e) {
log.warn("stop station realtime stream failed, station={} camera={} stationStreamId={} err={}",
station.getStationCode(), cameraCode, stationStreamId, e.getMessage());
}
}
private Object cameraLock(String stationCode, String cameraCode) {
String key = RealtimeStreamRegistry.cameraKey(stationCode, cameraCode);
return cameraLocks.computeIfAbsent(key, ignored -> new Object());
}
private static JsonNode successPayload(JsonNode root) {
if (root.has("code") && root.path("code").asInt(-1) != 200) {
String message = root.path("msg").asText(root.path("message").asText("站端接口返回失败"));
throw new IllegalStateException(message);
}
JsonNode payload = root.path("data");
return payload.isObject() ? payload : root;
}
private static String textAnyDeep(JsonNode node, String... fields) {
String value = textAny(node, fields);
if (!StringUtils.hasText(value) && node.path("data").isObject()) {
value = textAny(node.path("data"), fields);
}
return value;
}
private static String textAny(JsonNode node, String... fields) {
if (node == null || fields == null) {
return null;
}
for (String field : fields) {
String value = text(node, field);
if (StringUtils.hasText(value)) {
return value;
}
}
return null;
}
private static String text(JsonNode node, String field) {
JsonNode value = node == null ? null : node.get(field);
return value == null || value.isNull() ? null : value.asText();
}
/** 汇聚实时流 ID:站点 + 摄像机;去除 cameraCode 中 # 后的附属信息。 */
private static String aggStreamId(String stationCode, String cameraCode) {
String base = cameraCode.contains("#") ? cameraCode.substring(0, cameraCode.indexOf('#')) : cameraCode;
return (stationCode + "_" + base).replaceAll("[^A-Za-z0-9_]", "_");
}
private static String extractIp(String urlString) {
try {
return new URL(urlString).getHost();
} catch (MalformedURLException e) {
return null;
}
}
@Data
public static class StartResult {
private final String streamId;
private final boolean existing;
public StartResult(String streamId, boolean existing) {
this.streamId = streamId;
this.existing = existing;
}
}
}

+ 105
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeViewerRegistry.java View File

@ -0,0 +1,105 @@
package com.inspect.agg.video;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
/**
* 实时流观看会话注册表:一个 streamId 对应多个观看票据。
* start 增加一个票据,stop 只减少一个票据,最后一个观看者退出时才允许删除 ZLM 流。
*/
@Component
public class RealtimeViewerRegistry {
/** 观看票据过期时间;当前没有心跳接口,暂用 24 小时作为兜底清理。 */
private static final long TICKET_TTL_MILLIS = 24L * 60 * 60 * 1000;
private final Map<String, List<Long>> tickets = new ConcurrentHashMap<>();
private final Map<String, Long> lastActivity = new ConcurrentHashMap<>();
public void add(String streamId) {
tickets.compute(streamId, (key, current) -> {
List<Long> list = current == null ? new ArrayList<>() : current;
synchronized (list) {
list.add(System.currentTimeMillis());
}
return list;
});
}
/**
* 移除一个观看票据。
*
* @return -1 表示没有票据;0 表示本次移除后无人观看;大于 0 表示仍有 N 个观看者
*/
public int remove(String streamId) {
final int[] remaining = {-2};
tickets.compute(streamId, (key, current) -> {
if (current == null || current.isEmpty()) {
remaining[0] = -1;
return null;
}
synchronized (current) {
current.remove(current.size() - 1);
if (current.isEmpty()) {
remaining[0] = 0;
lastActivity.remove(streamId);
return null;
}
remaining[0] = current.size();
return current;
}
});
return remaining[0];
}
public void touch(String streamId) {
if (streamId != null) {
lastActivity.put(streamId, System.currentTimeMillis());
}
}
public long lastActivityAge(String streamId, long now) {
Long last = lastActivity.get(streamId);
return last == null ? Long.MAX_VALUE : now - last;
}
public void clear(String streamId) {
tickets.remove(streamId);
lastActivity.remove(streamId);
}
public int count(String streamId) {
List<Long> current = tickets.get(streamId);
if (current == null) {
return 0;
}
synchronized (current) {
return current.size();
}
}
/**
* 清理过期票据,返回因此没有观看者的 streamId 列表。
*/
public List<String> expire(long now) {
List<String> closed = new ArrayList<>();
for (String streamId : new ArrayList<>(tickets.keySet())) {
tickets.computeIfPresent(streamId, (key, current) -> {
synchronized (current) {
current.removeIf(ticketTime -> now - ticketTime > TICKET_TTL_MILLIS);
if (current.isEmpty()) {
closed.add(streamId);
lastActivity.remove(streamId);
return null;
}
return current;
}
});
}
return closed;
}
}

+ 18
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/StationStream.java View File

@ -0,0 +1,18 @@
package com.inspect.agg.video;
import lombok.Data;
/**
* 站端转流信息(来自 /ivs/api/v1/stream/start)。
*/
@Data
public class StationStream {
private String stationCode;
/** 站端 StreamID */
private String stationStreamId;
/** 站端 ZLM 的 HTTP-FLV / HLS 地址 */
private String flvUrl;
private String hlsUrl;
private long createdAt = System.currentTimeMillis();
}

+ 35
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/StationStreamRegistry.java View File

@ -0,0 +1,35 @@
package com.inspect.agg.video;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 会话内的站端流信息注册表(内存,进程重启后失效,前端重新调用 /play 即可)。
*/
@Component
public class StationStreamRegistry {
private final Map<String, StationStream> streams = new ConcurrentHashMap<>();
public static String key(String stationCode, String stationStreamId) {
return stationCode + "/" + stationStreamId;
}
public void put(StationStream stream) {
streams.put(key(stream.getStationCode(), stream.getStationStreamId()), stream);
}
public StationStream get(String stationCode, String streamId) {
if (!StringUtils.hasText(stationCode) || !StringUtils.hasText(streamId)) {
return null;
}
return streams.get(key(stationCode, streamId));
}
public void remove(String stationCode, String streamId) {
streams.remove(key(stationCode, streamId));
}
}

+ 170
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/ZlmApiClient.java View File

@ -0,0 +1,170 @@
package com.inspect.agg.video;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.inspect.agg.config.AggProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* ZLMediaKit API 客户端(汇聚侧)。
* 仅依赖文档化接口 getMediaList/addStreamProxy/delStreamProxy,
* 以及新版 ZLM 提供的 listStreamProxy(旧版本不支持时自动降级)。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ZlmApiClient {
private static final String DEFAULT_VHOST = "__defaultVhost__";
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private final AggProperties props;
/** 判断汇聚 ZLM 上在线媒体是否已存在(流已注册、可播放)。 */
public boolean streamExists(String stream) {
AggProperties.Video v = props.getVideo();
String url = UriComponentsBuilder.fromHttpUrl(api("/index/api/getMediaList"))
.queryParam("secret", v.getZlmSecret())
.queryParam("vhost", DEFAULT_VHOST)
.queryParam("app", v.getApp())
.queryParam("stream", stream)
.toUriString();
JsonNode root = getJson(url);
for (JsonNode n : root.path("data")) {
if (stream.equals(n.path("stream").asText())) {
return true;
}
}
return false;
}
/**
* 按 key 精确查询拉流代理(listStreamProxy)。
* 返回代理节点(含 key、src、url、status、status_str 等);不存在时返回 null。
* 代理可能正在重试或源不可达,此时在线媒体(getMediaList)查不到,但代理对象已存在。
* 旧版 ZLM 不支持该接口或调用异常时按“无代理”处理,交由 addStreamProxy 的幂等逻辑兜底。
*/
public JsonNode findStreamProxy(String stream) {
AggProperties.Video v = props.getVideo();
String key = proxyKey(stream);
String url = UriComponentsBuilder.fromHttpUrl(api("/index/api/listStreamProxy"))
.queryParam("secret", v.getZlmSecret())
.queryParam("key", key)
.toUriString();
try {
JsonNode root = getJson(url);
// 新版 ZLM 精确 key 未命中时返回 code=-400、key not found,按无代理处理。
if (root.path("code").asInt(-1) != 0) {
return null;
}
for (JsonNode n : root.path("data")) {
if (stream.equals(n.path("src").path("stream").asText())
|| key.equals(n.path("key").asText())) {
return n;
}
}
return null;
} catch (Exception e) {
log.warn("ZLM listStreamProxy 查询失败,按无代理处理: stream={} err={}", stream, e.getMessage());
return null;
}
}
/** 拉流代理配置是否已存在(不要求媒体已在线)。 */
public boolean proxyExists(String stream) {
return findStreamProxy(stream) != null;
}
/**
* 添加拉流代理,返回 ZLM 的 key。
* 若 ZLM 返回 “This stream already exists”(并发或残留同名代理),按幂等成功处理并返回确定性 key。
*/
public String addStreamProxy(String stream, String sourceUrl) {
AggProperties.Video v = props.getVideo();
String url = UriComponentsBuilder.fromHttpUrl(api("/index/api/addStreamProxy"))
.queryParam("secret", v.getZlmSecret())
.queryParam("vhost", DEFAULT_VHOST)
.queryParam("app", v.getApp())
.queryParam("stream", stream)
.queryParam("url", sourceUrl)
.queryParam("enable_hls", 1)
.queryParam("enable_rtsp", 1)
.queryParam("enable_rtmp", 1)
.queryParam("enable_mp4", 0)
.queryParam("rtsp_transport", v.getRtspTransport())
.queryParam("timeout_sec", 10)
.queryParam("retry_count", v.getRetryCount())
.toUriString();
JsonNode root = getJson(url);
if (root.path("code").asInt(-1) != 0) {
String msg = root.path("msg").asText();
if (msg != null && msg.toLowerCase().contains("already exists")) {
log.info("ZLM addStreamProxy 代理已存在,按复用处理: stream={} msg={}", stream, msg);
return proxyKey(stream);
}
throw new IllegalStateException("ZLM addStreamProxy 失败: " + msg);
}
return root.path("data").path("key").asText(proxyKey(stream));
}
/** 关闭拉流代理(删除代理对象,即使媒体尚未注册成功也可删除)。 */
public boolean delStreamProxy(String stream) {
AggProperties.Video v = props.getVideo();
String key = proxyKey(stream);
JsonNode root = getJson(UriComponentsBuilder.fromHttpUrl(api("/index/api/delStreamProxy"))
.queryParam("secret", v.getZlmSecret())
.queryParam("key", key)
.toUriString());
boolean ok = root.path("code").asInt(-1) == 0 && root.path("data").path("flag").asBoolean(false);
if (!ok) {
log.warn("ZLM delStreamProxy 未成功: stream={} resp={}", stream, root);
}
return ok;
}
/** 汇聚 HLS 播放地址。 */
public String hlsUrl(String stream) {
return publicBase() + "/" + props.getVideo().getApp() + "/" + stream + "/hls.m3u8";
}
/** 汇聚 HTTP-FLV 播放地址。 */
public String flvUrl(String stream) {
return publicBase() + "/" + props.getVideo().getApp() + "/" + stream + ".live.flv";
}
private String proxyKey(String stream) {
return DEFAULT_VHOST + "/" + props.getVideo().getApp() + "/" + stream;
}
private String publicBase() {
AggProperties.Video v = props.getVideo();
String base = v.getPublicBaseUrl() == null || v.getPublicBaseUrl().trim().isEmpty()
? v.getZlmBaseUrl() : v.getPublicBaseUrl();
return base.replaceAll("/+$", "");
}
private String api(String path) {
return props.getVideo().getZlmBaseUrl().replaceAll("/+$", "") + path;
}
private JsonNode getJson(String url) {
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET,
new HttpEntity<>(new HttpHeaders()), String.class);
try {
return objectMapper.readTree(resp.getBody());
} catch (Exception e) {
throw new IllegalStateException("ZLM 响应解析失败: " + e.getMessage());
}
}
}

+ 251
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggAreaController.java View File

@ -0,0 +1,251 @@
package com.inspect.agg.web;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.inspect.agg.entity.AggBasedataArea;
import com.inspect.agg.entity.AggBasedataDevice;
import com.inspect.agg.entity.AggBasedataDevicetype;
import com.inspect.agg.entity.AggBasedataEqpbook;
import com.inspect.agg.entity.AggBasedataEqpbookChannel;
import com.inspect.agg.entity.AggBasedataPatrolpoint;
import com.inspect.agg.entity.AggPatrolTask;
import com.inspect.agg.entity.AggPatrolTaskStatus;
import com.inspect.agg.mapper.AggBasedataAreaMapper;
import com.inspect.agg.mapper.AggBasedataDeviceMapper;
import com.inspect.agg.mapper.AggBasedataDevicetypeMapper;
import com.inspect.agg.mapper.AggBasedataEqpbookMapper;
import com.inspect.agg.mapper.AggBasedataEqpbookChannelMapper;
import com.inspect.agg.mapper.AggBasedataPatrolpointMapper;
import com.inspect.agg.mapper.AggPatrolTaskMapper;
import com.inspect.agg.mapper.AggPatrolTaskStatusMapper;
import com.inspect.agg.service.AreaSyncService;
import com.inspect.agg.service.ChannelSyncService;
import com.inspect.agg.service.DevicetypeSyncService;
import com.inspect.agg.service.DeviceSyncService;
import com.inspect.agg.service.EqpbookSyncService;
import com.inspect.agg.service.InspectionReportSyncService;
import com.inspect.agg.service.PatrolTaskSyncService;
import com.inspect.agg.service.PointSyncService;
import com.inspect.agg.service.AggAnalysisDefectMonthSyncService;
import com.inspect.agg.service.DefectRecordSyncService;
import com.inspect.agg.service.TaskExecSyncService;
import lombok.RequiredArgsConstructor;
/**
* 镜像数据查询与手动同步触发。
* 说明:查询全部走 Mapper XML(无 QueryWrapper);大屏不直连站点,只读中心库。
*/
@RestController
@RequestMapping("/api/agg")
@RequiredArgsConstructor
public class AggAreaController {
private final AggBasedataAreaMapper areaMapper;
private final AggBasedataDeviceMapper deviceMapper;
private final AggBasedataPatrolpointMapper pointMapper;
private final AggBasedataEqpbookMapper eqpbookMapper;
private final AggBasedataEqpbookChannelMapper channelMapper;
private final AggBasedataDevicetypeMapper devicetypeMapper;
private final AggPatrolTaskStatusMapper taskStatusMapper;
private final AggPatrolTaskMapper taskMapper;
private final AreaSyncService areaSyncService;
private final DeviceSyncService deviceSyncService;
private final PointSyncService pointSyncService;
private final EqpbookSyncService eqpbookSyncService;
private final ChannelSyncService channelSyncService;
private final DevicetypeSyncService devicetypeSyncService;
private final TaskExecSyncService taskExecSyncService;
private final PatrolTaskSyncService patrolTaskSyncService;
private final AggAnalysisDefectMonthSyncService resultAnalysisSyncService;
private final InspectionReportSyncService inspectionReportSyncService;
private final DefectRecordSyncService defectRecordSyncService;
/** 手动触发同步:/sync/{area|device|point|eqpbook|channel|devicetype|task|taskplan|analysis|report|all} */
@PostMapping("/sync/{entity}")
public Map<String, Object> sync(@PathVariable String entity) {
switch (entity) {
case "area":
areaSyncService.syncAllStations();
break;
case "device":
deviceSyncService.syncAllStations();
break;
case "point":
pointSyncService.syncAllStations();
break;
case "eqpbook":
eqpbookSyncService.syncAllStations();
break;
case "channel":
channelSyncService.syncAllStations();
break;
case "devicetype":
devicetypeSyncService.syncAllStations();
break;
case "task":
taskExecSyncService.syncFullAllStations();
break;
case "taskplan":
patrolTaskSyncService.syncFullAllStations();
break;
case "analysis":
resultAnalysisSyncService.syncLast12MonthsAllStations();
break;
case "report":
inspectionReportSyncService.syncAllStations();
break;
case "defect":
defectRecordSyncService.syncFullAllStations();
break;
default:
areaSyncService.syncAllStations();
deviceSyncService.syncAllStations();
pointSyncService.syncAllStations();
eqpbookSyncService.syncAllStations();
channelSyncService.syncAllStations();
devicetypeSyncService.syncAllStations();
taskExecSyncService.syncFullAllStations();
patrolTaskSyncService.syncFullAllStations();
resultAnalysisSyncService.syncLast12MonthsAllStations();
inspectionReportSyncService.syncAllStations();
defectRecordSyncService.syncFullAllStations();
break;
}
Map<String, Object> m = new HashMap<>();
m.put("code", 200);
m.put("msg", "sync triggered: " + entity);
return m;
}
@GetMapping("/area/list")
public Map<String, Object> areaList(@RequestParam(required = false) String stationCode,
@RequestParam(defaultValue = "500") int max) {
return ok(areaMapper.listByStation(stationCode, cap(max)),
areaMapper.countByStation(stationCode));
}
/** 区域树(可选 stationCode 过滤站点,可选 rootId 指定根区域生成子树) */
@GetMapping("/area/tree")
public Map<String, Object> areaTree(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) Long rootId) {
List<AggBasedataArea> all = areaMapper.listForTree(stationCode);
Set<Long> ids = all.stream().map(AggBasedataArea::getAreaId).collect(Collectors.toSet());
Map<Long, List<AggBasedataArea>> byParent = all.stream()
.collect(Collectors.groupingBy(a -> a.getParentId() == null ? 0L : a.getParentId()));
List<Map<String, Object>> roots = new ArrayList<>();
if (rootId != null) {
for (AggBasedataArea a : all) {
if (a.getAreaId() != null && a.getAreaId().equals(rootId)) {
roots.add(toNode(a, byParent));
break;
}
}
} else {
for (AggBasedataArea a : all) {
long pid = a.getParentId() == null ? 0L : a.getParentId();
if (pid == 0L || !ids.contains(pid)) {
roots.add(toNode(a, byParent));
}
}
}
Map<String, Object> m = ok(roots, all.size());
return m;
}
@GetMapping("/device/list")
public Map<String, Object> deviceList(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) Long areaId,
@RequestParam(defaultValue = "500") int max) {
return ok(deviceMapper.list(stationCode, areaId, cap(max)),
deviceMapper.count(stationCode, areaId));
}
@GetMapping("/point/list")
public Map<String, Object> pointList(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) Long deviceId,
@RequestParam(required = false) Long areaId,
@RequestParam(defaultValue = "500") int max) {
return ok(pointMapper.list(stationCode, deviceId, areaId, cap(max)),
pointMapper.count(stationCode, deviceId, areaId));
}
@GetMapping("/eqpbook/list")
public Map<String, Object> eqpbookList(@RequestParam(required = false) String stationCode,
@RequestParam(defaultValue = "500") int max) {
return ok(eqpbookMapper.list(stationCode, cap(max)), eqpbookMapper.count(stationCode));
}
@GetMapping("/channel/list")
public Map<String, Object> channelList(@RequestParam(required = false) String stationCode,
@RequestParam(defaultValue = "500") int max) {
return ok(channelMapper.list(stationCode, cap(max)), channelMapper.count(stationCode));
}
@GetMapping("/devicetype/list")
public Map<String, Object> devicetypeList(@RequestParam(required = false) String stationCode,
@RequestParam(defaultValue = "500") int max) {
return ok(devicetypeMapper.list(stationCode, cap(max)), devicetypeMapper.count(stationCode));
}
@GetMapping("/task/list")
public Map<String, Object> taskList(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) String taskState,
@RequestParam(defaultValue = "500") int max) {
return ok(taskStatusMapper.list(stationCode, taskState, cap(max)),
taskStatusMapper.count(stationCode, taskState));
}
@GetMapping("/taskplan/list")
public Map<String, Object> taskplanList(@RequestParam(required = false) String stationCode,
@RequestParam(defaultValue = "500") int max) {
return ok(taskMapper.list(stationCode, cap(max)), taskMapper.count(stationCode));
}
private static int cap(int max) {
return Math.max(1, Math.min(max, 5000));
}
private Map<String, Object> ok(Object data, long total) {
Map<String, Object> m = new HashMap<>();
m.put("code", 200);
m.put("total", total);
m.put("data", data);
return m;
}
private Map<String, Object> toNode(AggBasedataArea a, Map<Long, List<AggBasedataArea>> byParent) {
Map<String, Object> node = new LinkedHashMap<>();
node.put("areaId", a.getAreaId());
node.put("parentId", a.getParentId());
node.put("stationCode", a.getStationCode());
node.put("stationName", a.getStationName() != null ? a.getStationName() : a.getStationCode());
node.put("areaName", a.getAreaName());
node.put("areaCode", a.getAreaCode());
node.put("areaType", a.getAreaType());
node.put("orderNum", a.getOrderNum());
node.put("status", a.getStatus());
List<Map<String, Object>> children = new ArrayList<>();
for (AggBasedataArea c : byParent.getOrDefault(a.getAreaId(), Collections.<AggBasedataArea>emptyList())) {
children.add(toNode(c, byParent));
}
node.put("children", children);
return node;
}
}

+ 65
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggCalendarController.java View File

@ -0,0 +1,65 @@
package com.inspect.agg.web;
import java.util.List;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.inspect.agg.calendar.AggTaskCalendarService;
import lombok.RequiredArgsConstructor;
/**
* 汇聚侧任务日历查询:实际状态 + 规则生成的未来计划。
*/
@RestController
@RequestMapping("/agg/calender")
@RequiredArgsConstructor
public class AggCalendarController {
private final AggTaskCalendarService calendarService;
@GetMapping("/year")
public List<Map<String, Object>> year(@RequestParam Integer year,
@RequestParam(required = false) String stationCode) {
checkYear(year);
return calendarService.year(stationCode, year);
}
@GetMapping("/month")
public List<Map<String, Object>> month(@RequestParam Integer year,
@RequestParam Integer month,
@RequestParam(required = false) String stationCode) {
checkYear(year);
checkMonth(month);
return calendarService.month(stationCode, year, month);
}
@GetMapping("/day")
public List<Map<String, Object>> day(@RequestParam Integer year,
@RequestParam Integer month,
@RequestParam Integer day,
@RequestParam(required = false) String stationCode) {
checkYear(year);
checkMonth(month);
if (day == null || day < 1 || day > 31) {
throw new IllegalArgumentException("无效的日期");
}
return calendarService.day(stationCode, year, month, day);
}
private static void checkYear(Integer year) {
if (year == null || year < 1900 || year > 2100) {
throw new IllegalArgumentException("无效的年份");
}
}
private static void checkMonth(Integer month) {
if (month == null || month < 1 || month > 12) {
throw new IllegalArgumentException("无效的月份");
}
}
}

+ 55
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggDefectRecordController.java View File

@ -0,0 +1,55 @@
package com.inspect.agg.web;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.inspect.agg.entity.AggDefectRecord;
import com.inspect.agg.mapper.AggDefectRecordMapper;
import com.inspect.base.core.web.domain.AjaxResult;
import com.inspect.base.core.web.page.TableDataInfo;
import lombok.RequiredArgsConstructor;
/**
* Aggregated defect-record query, matching the station-side path and parameters.
*/
@RestController
@RequestMapping("/agg/analysis")
@RequiredArgsConstructor
public class AggDefectRecordController {
private final AggDefectRecordMapper defectRecordMapper;
@GetMapping("/defect/algNameOptions")
public AjaxResult algNameOptions(@RequestParam(required = false) String stationCode) {
return AjaxResult.success(defectRecordMapper.selectDistinctAlgNames(stationCode));
}
@GetMapping("/defect/list")
public TableDataInfo defectList(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) String pointName,
@RequestParam(required = false) String patrolTimeBegin,
@RequestParam(required = false) String patrolTimeEnd,
@RequestParam(required = false) String algName,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
int safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
int safePageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
PageHelper.startPage(safePageNum, safePageSize);
List<AggDefectRecord> list = defectRecordMapper.selectList(
stationCode, pointName, patrolTimeBegin, patrolTimeEnd, algName);
PageInfo<AggDefectRecord> pageInfo = new PageInfo<>(list);
TableDataInfo result = new TableDataInfo();
result.setCode(200);
result.setMsg("Query Ok");
result.setRows(list);
result.setTotal(pageInfo.getTotal());
return result;
}
}

+ 42
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggDeviceTreeController.java View File

@ -0,0 +1,42 @@
package com.inspect.agg.web;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.inspect.agg.service.AggDeviceTreeService;
import com.inspect.base.core.web.domain.AjaxResult;
import lombok.RequiredArgsConstructor;
/**
* 汇聚侧区域-设备关系树,兼容站端 relation 接口。
*/
@RestController
@RequestMapping("/basedata/prodevmnt/device")
@RequiredArgsConstructor
public class AggDeviceTreeController {
private final AggDeviceTreeService deviceTreeService;
@GetMapping("/treeAreaDeviceSelectFilterByPreset/relation")
public AjaxResult treeAreaDeviceSelectFilterByPresetRelation(
@RequestParam(required = false) String stationCode,
HttpServletRequest request) {
Map<String, String> params = new LinkedHashMap<>();
request.getParameterMap().forEach((key, values) -> {
if (!"stationCode".equals(key) && !"pageNum".equals(key) && !"pageSize".equals(key)
&& values != null && values.length > 0 && StringUtils.hasText(values[0])) {
params.put(key, values[0]);
}
});
return AjaxResult.success(deviceTreeService.loadRelationTree(stationCode, params));
}
}

+ 53
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggInspectionReportController.java View File

@ -0,0 +1,53 @@
package com.inspect.agg.web;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.inspect.agg.entity.AggInspectionReport;
import com.inspect.agg.mapper.AggInspectionReportMapper;
import com.inspect.base.core.web.page.TableDataInfo;
import lombok.RequiredArgsConstructor;
/**
* 汇聚巡视报告查询。
*/
@RestController
@RequestMapping({"/agg/insreport"})
@RequiredArgsConstructor
public class AggInspectionReportController {
private final AggInspectionReportMapper reportMapper;
@GetMapping("/list")
public TableDataInfo list(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) String cycleStartTime,
@RequestParam(required = false) String cycleEndTime,
@RequestParam(value = "inspection_task_name", required = false)
String inspectionTaskName,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
int safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
int safePageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
PageHelper.startPage(safePageNum, safePageSize);
List<AggInspectionReport> list = reportMapper.selectList(
stationCode, cycleStartTime, cycleEndTime, inspectionTaskName);
PageInfo<AggInspectionReport> pageInfo = new PageInfo<>(list);
return dataTable(list, pageInfo.getTotal());
}
private static TableDataInfo dataTable(List<?> rows, long total) {
TableDataInfo result = new TableDataInfo();
result.setCode(200);
result.setMsg("Query Ok");
result.setRows(rows);
result.setTotal(total);
return result;
}
}

+ 81
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolPointQueryController.java View File

@ -0,0 +1,81 @@
package com.inspect.agg.web;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.inspect.agg.dto.AggMonitorPatrolPointVo;
import com.inspect.agg.entity.AggAlgSubtype;
import com.inspect.agg.mapper.AggBasedataPatrolpointMapper;
import com.inspect.agg.mapper.BasedataAlgSubtypeMapper;
import com.inspect.base.core.utils.StringUtils;
import com.inspect.base.core.web.page.TableDataInfo;
import lombok.RequiredArgsConstructor;
/**
* Aggregated patrol point monitor relation query.
*/
@RestController
@RequestMapping("/agg/patrolpoint")
@RequiredArgsConstructor
public class AggPatrolPointQueryController {
private final AggBasedataPatrolpointMapper patrolPointMapper;
private final BasedataAlgSubtypeMapper basedataAlgSubtypeMapper;
@GetMapping("/list/monitor")
public TableDataInfo monitorRelationList(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) Long deviceId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
if (deviceId == null) {
return dataTable(Collections.emptyList(), 0L);
}
int safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
int safePageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
PageHelper.startPage(safePageNum, safePageSize);
List<AggMonitorPatrolPointVo> list = patrolPointMapper.selectMonitorRelationList(stationCode, deviceId);
fillAlgSubtypeName(list);
PageInfo<AggMonitorPatrolPointVo> pageInfo = new PageInfo<>(list);
return dataTable(list, pageInfo.getTotal());
}
private void fillAlgSubtypeName(List<AggMonitorPatrolPointVo> list) {
if (list == null || list.isEmpty()) {
return;
}
Map<String, String> algSubtypeNameMap = basedataAlgSubtypeMapper.selectEnabledList().stream()
.collect(Collectors.toMap(item -> String.valueOf(item.getAlgSubtypeId()),
AggAlgSubtype::getAlgSubtypeName, (oldValue, newValue) -> oldValue));
for (AggMonitorPatrolPointVo item : list) {
String algSubtypeIds = item.getAlgSubtypeIds();
if (StringUtils.isNotEmpty(algSubtypeIds)) {
String algSubtypeName = java.util.Arrays.stream(algSubtypeIds.split(","))
.map(String::trim)
.filter(id -> !id.isEmpty())
.map(algSubtypeNameMap::get)
.filter(name -> name != null && !name.isEmpty())
.collect(Collectors.joining(","));
item.setAlgSubtypeName(algSubtypeName);
}
}
}
private static TableDataInfo dataTable(List<?> rows, long total) {
TableDataInfo result = new TableDataInfo();
result.setCode(200);
result.setMsg("Query Ok");
result.setRows(rows);
result.setTotal(total);
return result;
}
}

+ 47
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolTaskController.java View File

@ -0,0 +1,47 @@
package com.inspect.agg.web;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.inspect.agg.entity.AggPatrolTask;
import com.inspect.agg.mapper.AggPatrolTaskMapper;
import com.inspect.base.core.web.page.TableDataInfo;
import lombok.RequiredArgsConstructor;
/**
* 汇聚侧巡视方案列表,兼容站端 /patrol/task/list。
*/
@RestController
@RequestMapping("/agg/task")
@RequiredArgsConstructor
public class AggPatrolTaskController {
private final AggPatrolTaskMapper taskMapper;
@GetMapping("/list")
public TableDataInfo list(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) String taskName,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
int safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
int safePageSize = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 500);
PageHelper.startPage(safePageNum, safePageSize);
List<AggPatrolTask> list = taskMapper.selectTaskList(stationCode, taskName);
PageInfo<AggPatrolTask> pageInfo = new PageInfo<>(list);
TableDataInfo result = new TableDataInfo();
result.setCode(200);
result.setMsg("Query Ok");
result.setRows(list);
result.setTotal(pageInfo.getTotal());
return result;
}
}

+ 50
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolTaskStatusController.java View File

@ -0,0 +1,50 @@
package com.inspect.agg.web;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.inspect.agg.entity.AggPatrolTaskStatus;
import com.inspect.agg.mapper.AggPatrolTaskStatusMapper;
import com.inspect.base.core.web.page.TableDataInfo;
import lombok.RequiredArgsConstructor;
/**
* 汇聚侧巡视任务执行列表,兼容站端 /patrol/taskstatus/list 的分页返回协议。
*/
@RestController
@RequestMapping("/agg/taskstatus")
@RequiredArgsConstructor
public class AggPatrolTaskStatusController {
private final AggPatrolTaskStatusMapper taskStatusMapper;
@GetMapping("/list")
public TableDataInfo list(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) String taskName,
@RequestParam(required = false) String beginTime,
@RequestParam(required = false) String endTime,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
int safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
int safePageSize = pageSize == null || pageSize < 1 ? 10 : pageSize;
PageHelper.startPage(safePageNum, safePageSize);
List<AggPatrolTaskStatus> list = taskStatusMapper.selectTaskStatusList(
stationCode, taskName, beginTime, endTime);
PageInfo<AggPatrolTaskStatus> pageInfo = new PageInfo<>(list);
TableDataInfo result = new TableDataInfo();
result.setCode(200);
result.setMsg("Query Ok");
result.setRows(list);
result.setTotal(pageInfo.getTotal());
return result;
}
}

+ 34
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggStationController.java View File

@ -0,0 +1,34 @@
package com.inspect.agg.web;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.mapper.AggStationMapper;
import com.inspect.base.core.web.domain.AjaxResult;
import lombok.RequiredArgsConstructor;
/**
* 汇聚站点信息查询。
*/
@RestController
@RequestMapping("/agg/station")
@RequiredArgsConstructor
public class AggStationController {
private final AggStationMapper stationMapper;
/**
* 查询站点信息;默认只返回启用站点。
*/
@GetMapping("/all")
public AjaxResult all(@RequestParam(required = false) Integer enabled) {
List<AggStation> stations = stationMapper.selectByEnabled(enabled);
return AjaxResult.success(stations);
}
}

+ 69
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggStatisticController.java View File

@ -0,0 +1,69 @@
package com.inspect.agg.web;
import com.inspect.agg.mapper.AggBasedataEqpbookChannelMapper;
import com.inspect.agg.service.AggStatisticService;
import com.inspect.base.core.utils.StringUtils;
import com.inspect.base.core.web.domain.AjaxResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/agg/statistic")
@RequiredArgsConstructor
public class AggStatisticController {
private final AggStatisticService statisticService;
private final AggBasedataEqpbookChannelMapper channelMapper;
@GetMapping("/defect")
public AjaxResult defectStatistic(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) String algCategory,
@RequestParam(defaultValue = "month") String dateType) {
try {
List<Map<String, Object>> data = statisticService.defectStatistic(
stationCode, algCategory, dateType);
return AjaxResult.success(data);
} catch (IllegalArgumentException e) {
return AjaxResult.error(e.getMessage());
}
}
@GetMapping("/inspection")
public AjaxResult inspectionStatistic(@RequestParam(required = false) String stationCode,
@RequestParam(required = false) String algCategory) {
try {
return AjaxResult.success(statisticService.inspectionStatistic(stationCode, algCategory));
} catch (IllegalArgumentException e) {
return AjaxResult.error(e.getMessage());
}
}
/**
* 统计不同 deviceSource 的通道数量。
* stationCode 为空时统计全部站点,传入时统计单站。
* NULL、空字符串和纯空白统一归入“其他”。
*/
@GetMapping("/channel")
public AjaxResult channelStatistic(@RequestParam(required = false) String stationCode) {
List<Map<String, Object>> items = channelMapper.countByDeviceSource(stationCode);
long totalCount = 0L;
for (Map<String, Object> item : items) {
Object count = item.get("count");
if (count instanceof Number) {
totalCount += ((Number) count).longValue();
}
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("totalCount", totalCount);
data.put("deviceSourceCounts", items);
return AjaxResult.success(data);
}
}

+ 56
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggTreeController.java View File

@ -0,0 +1,56 @@
package com.inspect.agg.web;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.inspect.agg.service.AggDeviceTreeService;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.inspect.agg.service.EqpbookTreeService;
import com.inspect.base.core.web.domain.AjaxResult;
import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletRequest;
/**
* 汇聚侧通道树,兼容站端 /basedata/eqpbook/getAreaEqubookTreeSelectForFilter。
*/
@RestController
@RequestMapping("/agg/tree")
@RequiredArgsConstructor
public class AggTreeController {
private final EqpbookTreeService eqpbookTreeService;
private final AggDeviceTreeService deviceTreeService;
@GetMapping("/channel")
public AjaxResult getAreaEqubookTreeSelectForFilter(
@RequestParam(value = "patroldeviceName", required = false) String patrolDeviceName,
@RequestParam(value = "onlineStatus", required = false) String onlineStatus,
@RequestParam(value = "patroldeviceTypeFlag", required = false) String patrolDeviceTypeFlag,
@RequestParam(required = false) String stationCode) {
List<Map<String, Object>> tree = eqpbookTreeService.buildFilterTree(
stationCode, patrolDeviceName, onlineStatus, patrolDeviceTypeFlag);
return AjaxResult.success(tree);
}
@GetMapping("/device")
public AjaxResult treeAreaDeviceSelectFilterByPresetRelation(
@RequestParam(required = false) String stationCode,
HttpServletRequest request) {
Map<String, String> params = new LinkedHashMap<>();
request.getParameterMap().forEach((key, values) -> {
if (!"stationCode".equals(key) && !"pageNum".equals(key) && !"pageSize".equals(key)
&& values != null && values.length > 0 && StringUtils.hasText(values[0])) {
params.put(key, values[0]);
}
});
return AjaxResult.success(deviceTreeService.loadRelationTree(stationCode, params));
}
}

+ 313
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggVideoController.java View File

@ -0,0 +1,313 @@
package com.inspect.agg.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.dto.VideoRequest;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.service.StationRegistry;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.video.PlaybackSession;
import com.inspect.agg.video.PlaybackSessionRegistry;
import com.inspect.agg.video.RealtimeStreamService;
import com.inspect.agg.video.ZlmApiClient;
import com.inspect.base.core.web.domain.AjaxResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 汇聚视频接口。
*/
@Slf4j
@RestController
@RequestMapping("/agg/video")
@RequiredArgsConstructor
public class AggVideoController {
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final ZlmApiClient zlmApiClient;
private final PlaybackSessionRegistry playbackSessionRegistry;
private final RealtimeStreamService realtimeStreamService;
private static String resolvePlaybackUrl(AggStation station, String url) {
if (url.startsWith("http://") || url.startsWith("https://")) {
return url;
}
try {
String base = station.getBaseUrl().replaceAll("/+$", "") + "/";
return URI.create(base).resolve(url).toString();
} catch (Exception e) {
throw new IllegalArgumentException("无法解析站端 hlsUrl: " + url);
}
}
private static String playbackStreamId(String stationCode, String sessionId) {
return (stationCode + "_playback_" + sessionId).replaceAll("[^A-Za-z0-9_]", "_");
}
private static String textAny(JsonNode node, String... fields) {
if (node == null || fields == null) {
return null;
}
for (String field : fields) {
String value = text(node, field);
if (StringUtils.hasText(value)) {
return value;
}
}
return null;
}
private static String text(JsonNode node, String field) {
JsonNode value = node == null ? null : node.get(field);
return value == null || value.isNull() ? null : value.asText();
}
/**
* 只获取 RTSP 地址,供调试和排障使用。
*/
@GetMapping("/rtsp")
public AjaxResult rtsp(@RequestParam String stationCode,
@RequestParam String cameraCode) {
if (!StringUtils.hasText(stationCode) || !StringUtils.hasText(cameraCode)) {
return AjaxResult.error(400, "stationCode / cameraCode 不能为空");
}
AggStation station;
try {
station = stationRegistry.requireByCode(stationCode);
} catch (IllegalArgumentException e) {
return AjaxResult.error(404, e.getMessage());
}
try {
JsonNode root = fetchStreamInfo(station, cameraCode, 2);
Map<String, Object> data = new LinkedHashMap<>();
data.put("stationCode", stationCode);
data.put("cameraCode", cameraCode);
data.put("rtspURL", text(root, "rtspURL"));
data.put("flvURL", text(root, "flvURL"));
data.put("hlsURL", text(root, "hlsURL"));
return AjaxResult.success(data);
} catch (Exception e) {
log.error("fetch rtsp failed: station={} camera={} err={}", stationCode, cameraCode, e.getMessage());
return AjaxResult.error(502, "获取 RTSP 地址失败: " + e.getMessage());
}
}
/**
* 启动实时视频流。
*/
@PostMapping("/realtime/start")
public AjaxResult start(@RequestParam String stationCode,
@RequestBody(required = false) VideoRequest request) {
if (request == null) {
return AjaxResult.error(400, "请求体不能为空");
}
String cameraCode = request.getCameraCode();
if (!StringUtils.hasText(stationCode) || !StringUtils.hasText(cameraCode)) {
return AjaxResult.error(400, "stationCode / cameraCode 不能为空");
}
try {
RealtimeStreamService.StartResult result = realtimeStreamService.start(stationCode, request);
String streamId = result.getStreamId();
String hlsUrl = cacheBustedHlsUrl(streamId);
String flvUrl = zlmApiClient.flvUrl(streamId);
Map<String, Object> data = new LinkedHashMap<>();
data.put("stationCode", stationCode);
data.put("cameraCode", cameraCode);
data.put("streamId", streamId);
data.put("sourceMode", "RTSP_DIRECT");
data.put("status", result.isExisting() ? "PLAYING" : "STARTING");
data.put("hlsUrl", hlsUrl);
data.put("flvUrl", flvUrl);
data.put("hls", hlsUrl);
data.put("flv", flvUrl);
if (!result.isExisting()) {
return AjaxResult.success(
"已开始拉流;HLS 清单生成前请优先播放 FLV,或稍后重试 HLS", data);
}
return AjaxResult.success(data);
} catch (IllegalArgumentException e) {
return AjaxResult.error(404, e.getMessage());
} catch (Exception e) {
log.error("video start failed: station={} camera={} err={}", stationCode, cameraCode, e.getMessage());
return AjaxResult.error(502, "启动视频流失败: " + e.getMessage());
}
}
/**
* 启动历史回放:站端创建 HLS 会话,汇聚 ZLM 拉取 HLS 并返回汇聚播放地址。
*/
@GetMapping("/playback/start")
public AjaxResult playbackStart(@RequestParam String stationCode,
@RequestParam String cameraCode,
@RequestParam(required = false, defaultValue = "2") Integer streamType,
@RequestParam String startTime,
@RequestParam String endTime) {
if (!StringUtils.hasText(stationCode) || !StringUtils.hasText(cameraCode)
|| !StringUtils.hasText(startTime) || !StringUtils.hasText(endTime)) {
return AjaxResult.error(400, "stationCode / cameraCode / startTime / endTime 不能为空");
}
AggStation station;
try {
station = stationRegistry.requireByCode(stationCode);
} catch (IllegalArgumentException e) {
return AjaxResult.error(404, e.getMessage());
}
try {
JsonNode root = stationApiClient.createHlsPlaybackSession(
station, cameraCode, streamType, startTime, endTime);
if (root.path("code").asInt(-1) != 200) {
throw new IllegalStateException("站端创建回放会话失败: "
+ root.path("msg").asText(root.path("message").asText()));
}
JsonNode payload = root.path("data").isObject() ? root.get("data") : root;
String sessionId = text(payload, "sessionId");
String hlsUrl = textAny(payload, "hlsUrl", "hlsURL", "HLS");
if (!StringUtils.hasText(sessionId)) {
throw new IllegalStateException("站端未返回 sessionId");
}
if (!StringUtils.hasText(hlsUrl)) {
throw new IllegalStateException("站端未返回 hlsUrl");
}
String playbackUrl = resolvePlaybackUrl(station, hlsUrl);
String streamId = playbackStreamId(stationCode, sessionId);
boolean exists = zlmApiClient.streamExists(streamId);
if (!exists) {
zlmApiClient.addStreamProxy(streamId, playbackUrl);
}
playbackSessionRegistry.add(stationCode, cameraCode, sessionId, streamId);
Map<String, Object> data = new LinkedHashMap<>();
data.put("stationCode", stationCode);
data.put("cameraCode", cameraCode);
data.put("sessionId", sessionId);
data.put("sessionStatus", text(payload, "status"));
data.put("streamId", streamId);
data.put("sourceMode", "HLS_PLAYBACK");
data.put("status", exists ? "PLAYING" : "STARTING");
data.put("generation", text(payload, "generation"));
data.put("speed", text(payload, "speed"));
data.put("actualTimeRange", payload.get("actualTimeRange"));
data.put("currentTime", text(payload, "currentTime"));
data.put("hls", cacheBustedHlsUrl(streamId));
data.put("flv", zlmApiClient.flvUrl(streamId));
if (!exists) {
return AjaxResult.success(
"已开始回放拉流;HLS 清单生成前请优先播放 FLV,或稍后重试 HLS", data);
}
return AjaxResult.success(data);
} catch (Exception e) {
log.error("video playback start failed: station={} camera={} err={}",
stationCode, cameraCode, e.getMessage());
return AjaxResult.error(502, "启动历史回放失败: " + e.getMessage());
}
}
/**
* 停止实时视频流。
*/
@PostMapping("/realtime/stop")
public AjaxResult stop(@RequestParam String stationCode,
@RequestBody(required = false) VideoRequest request) {
if (request == null) {
return AjaxResult.error(400, "请求体不能为空");
}
String cameraCode = request.getCameraCode();
if (!StringUtils.hasText(stationCode) || !StringUtils.hasText(cameraCode)) {
return AjaxResult.error(400, "stationCode / cameraCode 不能为空");
}
try {
int remaining = realtimeStreamService.stop(stationCode, request);
if (remaining == -1) {
return AjaxResult.success("未找到可停止的观看会话");
}
if (remaining > 0) {
return AjaxResult.success("仍有 " + remaining + " 个观看会话,共享流已保留", false);
}
return AjaxResult.success(true);
} catch (IllegalArgumentException e) {
return AjaxResult.error(404, e.getMessage());
} catch (Exception e) {
log.error("video stop failed: station={} camera={} err={}", stationCode, cameraCode, e.getMessage());
return AjaxResult.error(502, "停止视频流失败: " + e.getMessage());
}
}
/**
* 停止当前站点和摄像机的历史回放会话。
*/
@GetMapping("/playback/stop")
public AjaxResult playbackStop(@RequestParam String stationCode,
@RequestParam String cameraCode) {
if (!StringUtils.hasText(stationCode) || !StringUtils.hasText(cameraCode)) {
return AjaxResult.error(400, "stationCode / cameraCode 不能为空");
}
AggStation station;
try {
station = stationRegistry.requireByCode(stationCode);
} catch (IllegalArgumentException e) {
return AjaxResult.error(404, e.getMessage());
}
List<PlaybackSession> sessions = playbackSessionRegistry.list(stationCode, cameraCode);
if (sessions.isEmpty()) {
return AjaxResult.error(404, "未找到可停止的历史回放会话");
}
int stationStopped = 0;
int streamStopped = 0;
List<String> failures = new ArrayList<>();
for (PlaybackSession session : sessions) {
try {
JsonNode root = stationApiClient.controlHlsPlaybackSession(
station, session.getSessionId(), session.getCameraCode(), "STOP");
if (root.path("code").asInt(-1) != 200) {
throw new IllegalStateException(root.path("msg").asText(root.path("message").asText()));
}
stationStopped++;
} catch (Exception e) {
failures.add("station:" + session.getSessionId() + ":" + e.getMessage());
}
try {
if (zlmApiClient.delStreamProxy(session.getStreamId())) {
streamStopped++;
} else {
failures.add("stream:" + session.getStreamId());
}
} catch (Exception e) {
failures.add("stream:" + session.getStreamId() + ":" + e.getMessage());
}
playbackSessionRegistry.remove(stationCode, cameraCode, session.getSessionId());
}
if (!failures.isEmpty()) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("totalCount", sessions.size());
data.put("stationStoppedCount", stationStopped);
data.put("streamStoppedCount", streamStopped);
return AjaxResult.error("部分历史回放会话停止失败: " + String.join("; ", failures), data);
}
return AjaxResult.success(true);
}
/**
* 每次启动返回不同的 m3u8 URL,避免浏览器或播放器复用已过期的切片清单。
*/
private String cacheBustedHlsUrl(String streamId) {
String url = zlmApiClient.hlsUrl(streamId);
return url + (url.contains("?") ? "&" : "?") + "_ts=" + System.currentTimeMillis();
}
private JsonNode fetchStreamInfo(AggStation station, String cameraCode, Integer streamType) {
JsonNode root = stationApiClient.fetchRtspUrl(station, cameraCode, streamType);
if (!StringUtils.hasText(text(root, "rtspURL")) && cameraCode.contains("#")) {
root = stationApiClient.fetchRtspUrl(station, cameraCode.substring(0, cameraCode.indexOf('#')), streamType);
}
return root;
}
}

+ 146
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/StationProxyController.java View File

@ -0,0 +1,146 @@
package com.inspect.agg.web;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.service.StationRegistry;
import com.inspect.agg.station.StationApiClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 站点接口反向代理:调用方只传 stationCode,汇聚服务自动取站点配置 + token 转发,
* 并原样返回站点响应(含错误响应)。
*
* 用法示例:
* GET /agg/proxy/patrol/video/realtime/start?stationCode=shaoxing&patroldeviceCode=xxx&channelCode=yyy
*/
@Slf4j
@RestController
@RequiredArgsConstructor
public class StationProxyController {
private static final String PREFIX = "/agg/proxy/";
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final RestTemplate restTemplate;
private final AggProperties props;
@RequestMapping(value = "/agg/proxy/**", method = {RequestMethod.GET, RequestMethod.POST,
RequestMethod.PUT, RequestMethod.DELETE})
public ResponseEntity<byte[]> proxy(HttpServletRequest request) {
String stationCode = request.getParameter("stationCode");
if (!StringUtils.hasText(stationCode)) {
return error(400, "stationCode 不能为空");
}
String uri = request.getRequestURI();
String subPath = uri.startsWith(PREFIX) ? uri.substring(PREFIX.length()) : uri;
if (!StringUtils.hasText(subPath)) {
return error(400, "缺少站点侧接口路径");
}
if (!allowed(subPath)) {
return error(403, "该路径不在代理白名单内: " + subPath);
}
AggStation station;
try {
station = stationRegistry.requireByCode(stationCode);
} catch (IllegalArgumentException e) {
return error(404, e.getMessage());
}
String query = filterStationCode(request.getQueryString());
String targetUrl = stripTrailingSlash(station.getBaseUrl()) + "/" + subPath
+ (StringUtils.hasText(query) ? "?" + query : "");
HttpHeaders headers = new HttpHeaders();
String token = stationApiClient.ensureToken(station);
headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token);
String contentType = request.getContentType();
if (StringUtils.hasText(contentType)) {
headers.set(HttpHeaders.CONTENT_TYPE, contentType);
}
String accept = request.getHeader(HttpHeaders.ACCEPT);
if (StringUtils.hasText(accept)) {
headers.set(HttpHeaders.ACCEPT, accept);
}
byte[] body = readBody(request);
HttpMethod method = HttpMethod.valueOf(request.getMethod());
try {
ResponseEntity<byte[]> resp = restTemplate.exchange(targetUrl, method,
new HttpEntity<>(body, headers), byte[].class);
return relay(resp.getStatusCodeValue(), resp.getHeaders().getContentType(), resp.getBody());
} catch (HttpStatusCodeException e) {
// 站点返回 4xx/5xx 也原样透传
return relay(e.getRawStatusCode(), e.getResponseHeaders() == null ? null
: e.getResponseHeaders().getContentType(), e.getResponseBodyAsByteArray());
} catch (Exception e) {
log.error("proxy failed: station={} path={} err={}", stationCode, subPath, e.getMessage());
return error(502, "代理调用站点失败: " + e.getMessage());
}
}
private boolean allowed(String subPath) {
if (props.getProxy() == null || props.getProxy().getAllowedPrefixes() == null
|| props.getProxy().getAllowedPrefixes().isEmpty()) {
return true;
}
return props.getProxy().getAllowedPrefixes().stream().anyMatch(subPath::startsWith);
}
private static String filterStationCode(String rawQuery) {
if (!StringUtils.hasText(rawQuery)) {
return null;
}
return Arrays.stream(rawQuery.split("&"))
.filter(p -> !p.equals("stationCode") && !p.startsWith("stationCode="))
.collect(Collectors.joining("&"));
}
private static byte[] readBody(HttpServletRequest request) {
try {
return StreamUtils.copyToByteArray(request.getInputStream());
} catch (Exception e) {
return new byte[0];
}
}
private static ResponseEntity<byte[]> relay(int status, MediaType contentType, byte[] body) {
HttpHeaders headers = new HttpHeaders();
if (contentType != null) {
headers.setContentType(contentType);
}
return new ResponseEntity<>(body == null ? new byte[0] : body, headers, status);
}
private static ResponseEntity<byte[]> error(int status, String message) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String json = "{\"code\":" + status + ",\"msg\":\"" + message.replace("\"", "'") + "\"}";
return new ResponseEntity<>(json.getBytes(StandardCharsets.UTF_8), headers, status);
}
private static String stripTrailingSlash(String base) {
return base == null ? "" : base.replaceAll("/+$", "");
}
}

+ 358
- 0
inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/VideoProxyController.java View File

@ -0,0 +1,358 @@
package com.inspect.agg.web;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import com.fasterxml.jackson.databind.JsonNode;
import com.inspect.agg.config.AggProperties;
import com.inspect.agg.entity.AggStation;
import com.inspect.agg.service.StationRegistry;
import com.inspect.agg.station.StationApiClient;
import com.inspect.agg.video.StationStream;
import com.inspect.agg.video.StationStreamRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 方案 B:集控纯 HTTP 反向代理(不拉流、不转码)。
* 办公端只访问集控地址;集控把站端 HLS/FLV 流式转发,并重写 m3u8 中的分片地址。
*
* GET /agg/video/play?stationCode=&cameraCode= 启动站端转流并返回集控播放地址
* GET /agg/video/flv?stationCode=&streamId= HTTP-FLV 流式代理
* GET /agg/video/hls?stationCode=&streamId= HLS 清单(重写分片地址)
* GET /agg/video/segment?stationCode=&streamId=&u= HLS 分片流式代理
* GET /agg/video/proxy/stop?stationCode=&streamId= 停止站端转流
*/
@Slf4j
@RestController
@RequestMapping("/agg/video/stream")
@RequiredArgsConstructor
public class VideoProxyController {
private static final Pattern TAG_URI = Pattern.compile("URI=\"([^\"]+)\"");
private final StationRegistry stationRegistry;
private final StationApiClient stationApiClient;
private final StationStreamRegistry streamRegistry;
private final AggProperties props;
/** 启动播放:调站端 /v1/stream/start,返回经集控代理的 HLS/FLV 地址 */
@GetMapping("/play")
public Map<String, Object> play(@RequestParam String stationCode,
@RequestParam String cameraCode) {
if (!StringUtils.hasText(stationCode) || !StringUtils.hasText(cameraCode)) {
return error(400, "stationCode / cameraCode 不能为空");
}
AggStation station;
try {
station = stationRegistry.requireByCode(stationCode);
} catch (IllegalArgumentException e) {
return error(404, e.getMessage());
}
try {
JsonNode root = stationApiClient.fetchStreamStart(station, mainCode(cameraCode), 2);
if (!StringUtils.hasText(text(root, "rtspURL")) && !StringUtils.hasText(text(root, "FLV"))
&& !StringUtils.hasText(text(root, "HLS"))) {
root = stationApiClient.fetchStreamStart(station, cameraCode, 2);
}
String stationStreamId = text(root, "StreamID");
String flv = text(root, "FLV");
String hls = text(root, "HLS");
if (!StringUtils.hasText(stationStreamId)) {
return error(502, "站端未返回 StreamID: " + root);
}
StationStream stream = new StationStream();
stream.setStationCode(stationCode);
stream.setStationStreamId(stationStreamId);
stream.setFlvUrl(flv);
stream.setHlsUrl(hls);
streamRegistry.put(stream);
Map<String, Object> data = new LinkedHashMap<>();
data.put("stationCode", stationCode);
data.put("cameraCode", cameraCode);
data.put("streamId", stationStreamId);
data.put("mode", "HTTP_PROXY");
if (StringUtils.hasText(hls)) {
data.put("hls", "/api/agg/video/hls?stationCode=" + enc(stationCode) + "&streamId=" + enc(stationStreamId));
}
if (StringUtils.hasText(flv)) {
data.put("flv", "/api/agg/video/flv?stationCode=" + enc(stationCode) + "&streamId=" + enc(stationStreamId));
}
Map<String, Object> m = new LinkedHashMap<>();
m.put("code", 200);
m.put("data", data);
return m;
} catch (Exception e) {
log.error("video play failed: station={} camera={} err={}", stationCode, cameraCode, e.getMessage());
return error(502, "启动视频流失败: " + e.getMessage());
}
}
/** 停止站端转流 */
@GetMapping("/stop")
public Map<String, Object> stop(@RequestParam String stationCode, @RequestParam String streamId) {
AggStation station;
try {
station = stationRegistry.requireByCode(stationCode);
} catch (IllegalArgumentException e) {
return error(404, e.getMessage());
}
try {
stationApiClient.fetchStreamStop(station, streamId);
streamRegistry.remove(stationCode, streamId);
Map<String, Object> m = new LinkedHashMap<>();
m.put("code", 200);
m.put("data", true);
return m;
} catch (Exception e) {
return error(502, "停止视频流失败: " + e.getMessage());
}
}
/** HTTP-FLV 流式代理(不缓冲,边收边发) */
@GetMapping("/flv")
public ResponseEntity<StreamingResponseBody> flv(@RequestParam String stationCode,
@RequestParam String streamId) {
StationStream stream = streamRegistry.get(stationCode, streamId);
if (stream == null || !StringUtils.hasText(stream.getFlvUrl())) {
return ResponseEntity.status(404).build();
}
return streaming(stream.getFlvUrl(), "video/x-flv");
}
/** HLS 清单:拉取站端 m3u8 并重写分片地址到集控代理 */
@GetMapping("/hls")
public ResponseEntity<byte[]> hls(@RequestParam String stationCode, @RequestParam String streamId) {
StationStream stream = streamRegistry.get(stationCode, streamId);
if (stream == null || !StringUtils.hasText(stream.getHlsUrl())) {
return ResponseEntity.status(404).build();
}
try {
PlaylistResult pr = readHlsWithFallback(stream.getHlsUrl());
if (!pr.url.equals(stream.getHlsUrl())) {
// 记录实际生效的清单地址,后续分片按它的目录解析
stream.setHlsUrl(pr.url);
}
String rewritten = rewritePlaylist(pr.content, stationCode, streamId, pr.url);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, "application/vnd.apple.mpegurl")
.header(HttpHeaders.CACHE_CONTROL, "no-store")
.body(rewritten.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
log.error("hls proxy failed: stream={} err={}", streamId, e.getMessage());
return ResponseEntity.status(502).build();
}
}
/** HLS 分片流式代理 */
@GetMapping("/segment")
public ResponseEntity<StreamingResponseBody> segment(@RequestParam String stationCode,
@RequestParam String streamId,
@RequestParam("u") String uri) {
StationStream stream = streamRegistry.get(stationCode, streamId);
if (stream == null) {
return ResponseEntity.status(404).build();
}
String target = resolve(stream.getHlsUrl(), uri);
if (target == null) {
return ResponseEntity.status(403).build();
}
return streaming(target, null);
}
/** 把 m3u8 里所有 URI(分片/密钥/初始化段)改写成集控代理地址 */
private String rewritePlaylist(String playlist, String stationCode, String streamId, String hlsUrl) {
StringBuilder sb = new StringBuilder(playlist.length() + 256);
for (String line : playlist.split("\n")) {
String trimmed = line.trim();
if (trimmed.isEmpty()) {
continue;
}
if (trimmed.startsWith("#")) {
Matcher matcher = TAG_URI.matcher(trimmed);
if (matcher.find()) {
String rewritten = proxyUri(stationCode, streamId, matcher.group(1));
sb.append(trimmed, 0, matcher.start(1)).append(rewritten).append(trimmed.substring(matcher.end(1)));
} else {
sb.append(trimmed);
}
} else {
sb.append(proxyUri(stationCode, streamId, trimmed));
}
sb.append('\n');
}
return sb.toString();
}
private String proxyUri(String stationCode, String streamId, String uri) {
return "/api/agg/video/segment?stationCode=" + enc(stationCode)
+ "&streamId=" + enc(streamId) + "&u=" + enc(uri);
}
/** 解析分片绝对地址(仅允许与站端 HLS 同 host:port,避免开放代理) */
private String resolve(String hlsUrl, String uri) {
try {
URI base = URI.create(hlsUrl);
String baseDir = hlsUrl.substring(0, hlsUrl.lastIndexOf('/') + 1);
String target = uri.startsWith("http://") || uri.startsWith("https://") ? uri
: uri.startsWith("/") ? base.getScheme() + "://" + base.getAuthority() + uri
: baseDir + uri;
URI t = URI.create(target);
if (!base.getAuthority().equalsIgnoreCase(t.getAuthority())) {
log.warn("拒绝跨主机分片代理: {}", target);
return null;
}
return target;
} catch (Exception e) {
return null;
}
}
/** 流式转发:不缓冲,边读边写 */
private ResponseEntity<StreamingResponseBody> streaming(String url, String contentType) {
StreamingResponseBody body = outputStream -> {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(props.getHttp().getConnectTimeoutMs());
conn.setReadTimeout(props.getHttp().getReadTimeoutMs());
conn.setInstanceFollowRedirects(true);
conn.setRequestProperty("User-Agent", "inspect-aggregation-proxy");
if (StringUtils.hasText(contentType)) {
conn.setRequestProperty(HttpHeaders.ACCEPT, contentType);
}
try (InputStream in = conn.getInputStream()) {
copy(in, outputStream);
}
} catch (Exception e) {
log.warn("stream proxy interrupted: url={} err={}", url, e.getMessage());
} finally {
if (conn != null) {
conn.disconnect();
}
}
};
ResponseEntity.BodyBuilder builder = ResponseEntity.ok()
.header(HttpHeaders.CACHE_CONTROL, "no-store")
.header("X-Accel-Buffering", "no");
builder.header(HttpHeaders.CONTENT_TYPE,
StringUtils.hasText(contentType) ? contentType : MediaType.APPLICATION_OCTET_STREAM_VALUE);
return builder.body(body);
}
/**
* 兼容站端 HLS 路径风格差异:站端 start 接口可能返回 /live/{id}.m3u8(旧),
* 实际可用为 /live/{id}/hls.m3u8,反之亦然。
*/
private PlaylistResult readHlsWithFallback(String hlsUrl) throws Exception {
try {
return new PlaylistResult(readString(hlsUrl), hlsUrl);
} catch (Exception first) {
for (String alt : hlsAlternatives(hlsUrl)) {
try {
log.info("HLS 主地址不可用,尝试备用地址: {}", alt);
return new PlaylistResult(readString(alt), alt);
} catch (Exception ignore) {
// 继续尝试
}
}
throw first;
}
}
/** 清单内容 + 实际生效的清单地址 */
private static final class PlaylistResult {
private final String content;
private final String url;
PlaylistResult(String content, String url) {
this.content = content;
this.url = url;
}
}
private static java.util.List<String> hlsAlternatives(String hlsUrl) {
java.util.List<String> list = new java.util.ArrayList<>();
if (hlsUrl == null) {
return list;
}
if (hlsUrl.endsWith("/hls.m3u8")) {
// /live/{id}/hls.m3u8 -> /live/{id}.m3u8
String base = hlsUrl.substring(0, hlsUrl.length() - "/hls.m3u8".length());
list.add(base + ".m3u8");
} else if (hlsUrl.endsWith(".m3u8")) {
// /live/{id}.m3u8 -> /live/{id}/hls.m3u8
String base = hlsUrl.substring(0, hlsUrl.length() - ".m3u8".length());
list.add(base + "/hls.m3u8");
}
return list;
}
private String readString(String url) throws Exception {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(props.getHttp().getConnectTimeoutMs());
conn.setReadTimeout(props.getHttp().getReadTimeoutMs());
conn.setRequestProperty("User-Agent", "inspect-aggregation-proxy");
try (InputStream in = conn.getInputStream()) {
java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
copy(in, bos);
return new String(bos.toByteArray(), StandardCharsets.UTF_8);
} finally {
conn.disconnect();
}
}
private static void copy(InputStream in, OutputStream out) throws Exception {
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
out.flush();
}
}
private static String mainCode(String cameraCode) {
return cameraCode.contains("#") ? cameraCode.substring(0, cameraCode.indexOf('#')) : cameraCode;
}
private static String enc(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
} catch (Exception e) {
return s;
}
}
private static String text(JsonNode node, String field) {
JsonNode v = node == null ? null : node.get(field);
return v == null || v.isNull() ? null : v.asText();
}
private static Map<String, Object> error(int code, String msg) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("code", code);
m.put("msg", msg);
return m;
}
}

+ 34
- 0
inspect-main/inspect-main-agg/src/main/resources/application.yml View File

@ -0,0 +1,34 @@
server:
port: 18088
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/inspect_agg?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: htjc2018
redis:
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
database: ${REDIS_DATABASE:0}
timeout: 3000ms
mybatis:
mapper-locations: classpath*:mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.inspect.agg: info
agg:
video:
zlm-base-url: http://192.168.1.116:33380
zlm-secret: Xg@123456
app: live
public-base-url: http://192.168.1.116:33380
rtsp-transport: 0
retry-count: -1

+ 429
- 0
inspect-main/inspect-main-agg/src/main/resources/db/schema.sql View File

@ -0,0 +1,429 @@
-- =============================================================
-- 中心库 inspect_agg 初始化脚本(区域试点)
-- 执行:mysql -h127.0.0.1 -uroot -p inspect_agg < schema.sql
-- 表名约定:中心镜像表 = 站点源表名 + _agg(如 agg_basedata_area);agg_station/agg_sync_log 为内部表
-- =============================================================
-- 区域镜像表(源表 basedata_area)
CREATE TABLE IF NOT EXISTS agg_basedata_area (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
area_id BIGINT NOT NULL COMMENT '源区域 id',
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父区域 id',
ancestors VARCHAR(255) DEFAULT '' COMMENT '祖级列表,如 ,1,2002',
area_name VARCHAR(64) DEFAULT '' COMMENT '区域名称',
area_code VARCHAR(64) DEFAULT NULL COMMENT '区域编码',
area_addr VARCHAR(255) DEFAULT NULL COMMENT '区域路径',
area_type VARCHAR(64) DEFAULT NULL COMMENT 'station/station_in/bayunit',
device_type_code VARCHAR(255) DEFAULT NULL COMMENT '生产区域编码',
order_num INT DEFAULT 0 COMMENT '显示顺序',
status CHAR(1) DEFAULT '0' COMMENT '0正常 1停用',
remark VARCHAR(255) DEFAULT NULL COMMENT '描述',
update_time DATETIME DEFAULT NULL COMMENT '站点侧 update_time',
synced_at DATETIME NOT NULL COMMENT '中心落库时间',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0存在 1站点侧已删除(对账标记)',
PRIMARY KEY (id),
UNIQUE KEY uk_station_area (station_code, area_id),
KEY idx_station_parent (station_code, parent_id),
KEY idx_station_deleted (station_code, is_deleted)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '区域镜像表';
-- 同步运行日志
CREATE TABLE IF NOT EXISTS agg_sync_log (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
entity VARCHAR(32) NOT NULL DEFAULT 'area' COMMENT '同步实体',
sync_type VARCHAR(16) DEFAULT 'FULL' COMMENT 'FULL/INCREMENT',
status VARCHAR(16) DEFAULT NULL COMMENT 'SUCCESS/FAILED',
row_count INT DEFAULT 0 COMMENT '本次拉取行数',
deleted_count INT DEFAULT 0 COMMENT '对账标记删除数',
message VARCHAR(255) DEFAULT NULL COMMENT '结果或错误信息',
started_at DATETIME DEFAULT NULL,
finished_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_station_entity (station_code, entity, started_at)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '同步运行日志';
-- =============================================================
-- 设备 / 点位 镜像表(基础数据,第二批)
-- =============================================================
-- 设备镜像表(源表 basedata_device,接口 /basedata/prodevmnt/device/list)
CREATE TABLE IF NOT EXISTS agg_basedata_device (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
device_id BIGINT NOT NULL COMMENT '源设备 id',
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父设备 id',
ancestors VARCHAR(255) DEFAULT '' COMMENT '祖级列表',
area_id BIGINT DEFAULT NULL COMMENT '区域 id',
area_name VARCHAR(128) DEFAULT NULL COMMENT '区域名称',
device_code VARCHAR(64) DEFAULT NULL COMMENT '设备编号',
device_name VARCHAR(200) DEFAULT NULL COMMENT '设备名称',
device_type_name VARCHAR(64) DEFAULT NULL COMMENT '设备类型名称',
device_model VARCHAR(64) DEFAULT NULL COMMENT '设备型号',
manufacturer VARCHAR(64) DEFAULT NULL COMMENT '生产厂家',
power_main_equ_flag VARCHAR(5) DEFAULT NULL COMMENT '电力主设备标识 Yes/No',
station_no VARCHAR(32) DEFAULT NULL COMMENT '场站编号',
monitor_index_no VARCHAR(32) DEFAULT NULL COMMENT '监控索引号',
status CHAR(1) DEFAULT '0' COMMENT '0正常 1停用',
order_num INT DEFAULT 0 COMMENT '显示顺序',
update_time DATETIME DEFAULT NULL COMMENT '站点侧 update_time',
synced_at DATETIME NOT NULL COMMENT '中心落库时间',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0存在 1站点侧已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_station_device (station_code, device_id),
KEY idx_device_area (station_code, area_id),
KEY idx_device_deleted (station_code, is_deleted)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '设备镜像表';
-- 巡检点位镜像表(源表 basedata_patrolpoint,接口 /basedata/patrolpointmnt/patrolpoint/list)
CREATE TABLE IF NOT EXISTS agg_basedata_patrolpoint (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
patrol_point_id BIGINT NOT NULL COMMENT '源点位 id',
patrol_point_code VARCHAR(255) DEFAULT NULL COMMENT '点位编码',
patrol_point_name VARCHAR(255) DEFAULT NULL COMMENT '点位名称',
main_device_id BIGINT DEFAULT NULL COMMENT '主设备 id',
device_id BIGINT DEFAULT NULL COMMENT '设备 id(真正配置点位的设备/部件)',
area_id BIGINT DEFAULT NULL COMMENT '区域 id',
area_name VARCHAR(128) DEFAULT NULL COMMENT '区域名称',
patrol_content VARCHAR(255) DEFAULT NULL COMMENT '巡检内容',
purpose VARCHAR(255) DEFAULT NULL COMMENT '用途',
patrol_type_id VARCHAR(255) DEFAULT NULL COMMENT '巡视类型(逗号分隔)',
point_impt_level VARCHAR(10) DEFAULT NULL COMMENT '重要等级 Ⅰ/Ⅱ/Ⅲ',
alarm_threshold TEXT DEFAULT NULL COMMENT '告警阈值(JSON)',
alg_subtype_ids VARCHAR(200) DEFAULT NULL COMMENT 'Algorithm subtype IDs, comma-separated',
update_time DATETIME DEFAULT NULL COMMENT '站点侧 update_time',
synced_at DATETIME NOT NULL COMMENT '中心落库时间',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0存在 1站点侧已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_station_point (station_code, patrol_point_id),
KEY idx_point_device (station_code, device_id),
KEY idx_point_area (station_code, area_id),
KEY idx_point_deleted (station_code, is_deleted)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '巡检点位镜像表';
-- =============================================================
-- 台账 / 通道 / 设备类型 镜像表(基础数据,第三批)
-- =============================================================
-- 设备台账镜像表(源表 basedata_eqpbook,接口 /basedata/eqpbook/list)
CREATE TABLE IF NOT EXISTS agg_basedata_eqpbook (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
eqpbook_id BIGINT NOT NULL COMMENT '源台账 id',
patrol_device_code VARCHAR(255) DEFAULT NULL COMMENT '巡视设备编码',
patrol_device_name VARCHAR(255) DEFAULT NULL COMMENT '设备名称',
station_name VARCHAR(255) DEFAULT NULL COMMENT '变电站名称',
area_id BIGINT DEFAULT NULL COMMENT '区域 id',
area_name VARCHAR(255) DEFAULT NULL COMMENT '区域名称',
device_source VARCHAR(255) DEFAULT NULL COMMENT '设备来源',
device_model VARCHAR(255) DEFAULT NULL COMMENT '设备型号',
manufacturer VARCHAR(255) DEFAULT NULL COMMENT '生产厂家',
device_type VARCHAR(64) DEFAULT NULL COMMENT '设备类型(type)',
ip_addr VARCHAR(255) DEFAULT NULL COMMENT 'ip 地址',
port VARCHAR(64) DEFAULT NULL COMMENT '端口',
video_nvr_code VARCHAR(255) DEFAULT NULL COMMENT '所属 NVR 编码',
main_system_code VARCHAR(255) DEFAULT NULL COMMENT '主系统编号',
use_mode VARCHAR(255) DEFAULT NULL COMMENT '使用类型',
video_mode VARCHAR(255) DEFAULT NULL COMMENT '视频类型',
update_time DATETIME DEFAULT NULL COMMENT '站点侧 update_time',
synced_at DATETIME NOT NULL COMMENT '中心落库时间',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0存在 1站点侧已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_station_eqpbook (station_code, eqpbook_id),
KEY idx_eqpbook_area (station_code, area_id),
KEY idx_eqpbook_deleted (station_code, is_deleted)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '设备台账镜像表';
-- 设备通道镜像表(源表 basedata_eqpbook_channel,接口 /basedata/eqpbook/channel/list)
CREATE TABLE IF NOT EXISTS agg_basedata_eqpbook_channel (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
channel_id BIGINT NOT NULL COMMENT '源通道 id',
channel_code VARCHAR(255) DEFAULT NULL COMMENT '通道编码',
channel_name VARCHAR(255) DEFAULT NULL COMMENT '通道名称',
channel_type VARCHAR(255) DEFAULT NULL COMMENT '通道类型 vl/ir/ptz/ng',
online_state VARCHAR(4) DEFAULT NULL COMMENT '在线状态 1在线 0离线',
channel_content VARCHAR(255) DEFAULT NULL COMMENT '通道内容',
host VARCHAR(64) DEFAULT NULL COMMENT '主机 ip',
port VARCHAR(16) DEFAULT NULL COMMENT '端口',
area_id BIGINT DEFAULT NULL COMMENT '区域 id',
area_name VARCHAR(255) DEFAULT NULL COMMENT '区域名称',
eqpbook_id BIGINT DEFAULT NULL COMMENT '所属台账 id',
patrol_device_code VARCHAR(255) DEFAULT NULL COMMENT '巡视设备编码',
up_system_device_code VARCHAR(255) DEFAULT NULL COMMENT '上级系统编码',
device_source VARCHAR(255) DEFAULT NULL COMMENT '设备来源',
update_time DATETIME DEFAULT NULL COMMENT '站点侧 update_time',
synced_at DATETIME NOT NULL COMMENT '中心落库时间',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0存在 1站点侧已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_station_channel (station_code, channel_id),
KEY idx_channel_eqpbook (station_code, eqpbook_id),
KEY idx_channel_deleted (station_code, is_deleted)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '设备通道镜像表';
-- 设备类型镜像表(源表 basedata_devicetype,接口 /basedata/devicetype/list)
CREATE TABLE IF NOT EXISTS agg_basedata_devicetype (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
device_type_id BIGINT NOT NULL COMMENT '源设备类型 id',
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父类型 id',
order_num INT DEFAULT 0 COMMENT '显示顺序',
ancestors VARCHAR(255) DEFAULT '' COMMENT '祖级列表',
ancestors_addr VARCHAR(255) DEFAULT NULL COMMENT '祖级路径',
device_type_name VARCHAR(64) DEFAULT NULL COMMENT '类型名称',
device_type_code VARCHAR(255) DEFAULT NULL COMMENT '类型编码',
power_main_equ_flag VARCHAR(5) DEFAULT NULL COMMENT '电力主设备标识 Yes/No',
phase_num VARCHAR(10) DEFAULT NULL COMMENT '相数',
phase VARCHAR(10) DEFAULT NULL COMMENT '相别',
status CHAR(1) DEFAULT '0' COMMENT '0正常 1停用',
profession_type VARCHAR(10) DEFAULT NULL COMMENT '专业类型',
level BIGINT DEFAULT NULL COMMENT '层级',
update_time DATETIME DEFAULT NULL COMMENT '站点侧 update_time',
synced_at DATETIME NOT NULL COMMENT '中心落库时间',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0存在 1站点侧已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_station_devicetype (station_code, device_type_id),
KEY idx_devicetype_deleted (station_code, is_deleted)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '设备类型镜像表';
-- =============================================================
-- 站点配置表 agg_station(DB 维护,替代 yaml agg.sites)
-- =============================================================
CREATE TABLE IF NOT EXISTS agg_station (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码(中心唯一)',
station_name VARCHAR(64) NOT NULL COMMENT '站点名称',
base_url VARCHAR(255) NOT NULL COMMENT '站点 API 根地址,以 / 结尾',
portal_url VARCHAR(255) DEFAULT NULL COMMENT '站端门户网址',
username VARCHAR(64) NOT NULL COMMENT '对接账号',
password VARCHAR(128) DEFAULT NULL COMMENT '密码(临时明文,正式化前轮换/加密)',
enabled TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用',
PRIMARY KEY (id),
UNIQUE KEY uk_station_code (station_code)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '站点配置表';
-- 种子:绍兴站(联调用;生产请按实际修改并轮换密码)
INSERT IGNORE INTO agg_station (station_code, station_name, base_url, username, password, enabled)
VALUES ('CZ000000004', '白杨坪风光电场', 'http://192.168.131.10:18889/api/', 'admin', 'admin@123', 1);
-- =============================================================
-- 动态数据镜像表(任务执行 / 结果主表)
-- 站点侧无 update_time:不做时间水位;活跃子集高频拉取 + 全量慢周期对账
-- =============================================================
-- 任务执行状态镜像表(源表 patrol_task_status,接口 /patrol/taskstatus/list)
CREATE TABLE IF NOT EXISTS agg_patrol_task_status (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
line_id BIGINT NOT NULL COMMENT '源 line_id',
task_patrolled_id VARCHAR(255) DEFAULT NULL COMMENT '巡视任务执行ID',
task_name VARCHAR(255) DEFAULT NULL COMMENT '任务名称',
task_code VARCHAR(255) DEFAULT NULL COMMENT '任务编码',
task_state VARCHAR(16) DEFAULT NULL COMMENT '1已执行 2执行中 3暂停 4终止 5未执行 6超期',
plan_start_time DATETIME DEFAULT NULL COMMENT '计划开始时间',
start_time DATETIME DEFAULT NULL COMMENT '开始时间',
end_time DATETIME DEFAULT NULL COMMENT '结束时间',
task_progress VARCHAR(64) DEFAULT NULL COMMENT '任务进度',
task_estimated_time VARCHAR(64) DEFAULT NULL COMMENT '预计剩余时间',
patrol_type VARCHAR(64) DEFAULT NULL COMMENT '巡视类型',
pos_type VARCHAR(16) DEFAULT NULL COMMENT '0视频 1机器人 2无人机',
code VARCHAR(255) DEFAULT NULL COMMENT '机器编码',
pos VARCHAR(255) DEFAULT NULL COMMENT '机器预置位号',
exec_type VARCHAR(16) DEFAULT NULL COMMENT '执行类型 1系统 0手动',
task_type VARCHAR(16) DEFAULT NULL COMMENT '0普通 1主 2子',
main_id VARCHAR(255) DEFAULT NULL COMMENT '主任务编码',
qx_num INT NOT NULL DEFAULT 0 COMMENT '缺陷点位数',
file_status VARCHAR(16) NOT NULL DEFAULT '0' COMMENT '归档状态 0未归档 1已归档',
summary VARCHAR(255) DEFAULT NULL COMMENT 'summary(JSON 摘要)',
description VARCHAR(255) DEFAULT NULL COMMENT '描述',
create_time DATETIME DEFAULT NULL COMMENT '站点侧 create_time',
synced_at DATETIME NOT NULL COMMENT '中心落库时间',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0存在 1对账删除',
PRIMARY KEY (id),
UNIQUE KEY uk_station_line (station_code, line_id),
KEY idx_task_state (station_code, task_state),
KEY idx_task_deleted (station_code, is_deleted)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务执行状态镜像表';
-- 结果主表镜像表(源表 patrol_task_result_main,接口 /patrol/resultmain/list)
-- =============================================================
-- 任务方案镜像表(源 patrol_task,接口 /patrol/task/list)
-- =============================================================
CREATE TABLE IF NOT EXISTS agg_patrol_task (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
task_id BIGINT NOT NULL COMMENT '源任务 id',
task_code VARCHAR(200) DEFAULT NULL COMMENT '任务编码',
task_name VARCHAR(255) DEFAULT NULL COMMENT '任务名称',
type VARCHAR(255) DEFAULT NULL COMMENT '巡视类型',
priority VARCHAR(255) DEFAULT NULL COMMENT '优先级',
device_level VARCHAR(255) DEFAULT NULL COMMENT '设备层级',
device_list TEXT DEFAULT NULL COMMENT '设备列表(逗号分隔)',
fixed_start_time DATETIME DEFAULT NULL COMMENT '定期开始时间',
is_enable VARCHAR(16) DEFAULT NULL COMMENT '0可用 1不可用',
file_status VARCHAR(16) DEFAULT NULL COMMENT '状态',
execution_status VARCHAR(16) DEFAULT NULL COMMENT '执行状态',
task_cycle VARCHAR(255) DEFAULT NULL COMMENT '执行任务周期',
cycle_month VARCHAR(255) DEFAULT NULL COMMENT '周期-每月日期',
cycle_week VARCHAR(255) DEFAULT NULL COMMENT '周期-星期',
cycle_execute_time VARCHAR(255) DEFAULT NULL COMMENT '周期-执行时间',
cycle_start_time DATETIME DEFAULT NULL COMMENT '周期开始时间',
cycle_end_time DATETIME DEFAULT NULL COMMENT '周期结束时间',
interval_number INT DEFAULT NULL COMMENT '间隔数量',
interval_type VARCHAR(16) DEFAULT NULL COMMENT '间隔类型',
interval_execute_time VARCHAR(64) DEFAULT NULL COMMENT '间隔执行时间',
interval_start_time DATETIME DEFAULT NULL COMMENT '间隔开始时间',
interval_end_time DATETIME DEFAULT NULL COMMENT '间隔结束时间',
invalid_start_time DATETIME DEFAULT NULL COMMENT '不可用开始时间',
invalid_end_time DATETIME DEFAULT NULL COMMENT '不可用结束时间',
dev_type VARCHAR(255) DEFAULT NULL COMMENT '1机器人 0无人机 2视频',
area_id VARCHAR(255) DEFAULT NULL COMMENT '区域id',
area_name VARCHAR(255) DEFAULT NULL COMMENT '区域名称',
point_type VARCHAR(255) DEFAULT NULL COMMENT '算法大类 dict_code,多个值逗号分隔',
total_points INT NOT NULL DEFAULT 0 COMMENT '方案点位总数',
execution_mode VARCHAR(255) DEFAULT NULL COMMENT '执行模式',
drafter VARCHAR(255) DEFAULT NULL COMMENT '编制人',
specified_alg VARCHAR(255) DEFAULT NULL COMMENT '指定算法',
create_time DATETIME DEFAULT NULL COMMENT '站点侧 create_time',
update_time DATETIME DEFAULT NULL COMMENT '站点侧 update_time',
synced_at DATETIME NOT NULL COMMENT '中心落库时间',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0存在 1对账删除',
PRIMARY KEY (id),
UNIQUE KEY uk_station_task (station_code, task_id),
KEY idx_task_deleted (station_code, is_deleted),
KEY idx_task_plan (station_code, is_enable, execution_status)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '任务方案镜像表';
-- =============================================================
-- 巡检分析结果镜像表(源表 result_analysis)
-- =============================================================
CREATE TABLE IF NOT EXISTS agg_result_analysis (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
line_id BIGINT NOT NULL COMMENT '源 line_id',
result_type VARCHAR(16) DEFAULT NULL COMMENT '0一般/严重/重大缺陷 1正常 2轻微缺陷',
business_id VARCHAR(255) DEFAULT NULL COMMENT '关联业务 ID',
result_content LONGTEXT DEFAULT NULL COMMENT '原始结果内容',
res_status VARCHAR(16) DEFAULT NULL COMMENT '结果状态',
point_status VARCHAR(16) DEFAULT NULL COMMENT '点位处理状态',
suggestion VARCHAR(255) DEFAULT NULL COMMENT '处理建议',
object_id VARCHAR(255) DEFAULT NULL COMMENT '点位/对象 ID',
description VARCHAR(255) DEFAULT NULL COMMENT '结果描述',
request_id VARCHAR(255) DEFAULT NULL COMMENT '请求 ID',
task_patrol_id VARCHAR(255) DEFAULT NULL COMMENT '巡检任务执行 ID',
alg_type VARCHAR(255) DEFAULT NULL COMMENT '算法类型',
res_value VARCHAR(255) DEFAULT NULL COMMENT '识别值',
res_code VARCHAR(64) DEFAULT NULL COMMENT '返回码',
res_conf VARCHAR(64) DEFAULT NULL COMMENT '置信度',
res_desc VARCHAR(255) DEFAULT NULL COMMENT '算法描述',
result VARCHAR(16) DEFAULT NULL COMMENT '结果标识',
res_img_url VARCHAR(255) DEFAULT NULL COMMENT '结果图片地址',
image_normal_url_path VARCHAR(255) DEFAULT NULL COMMENT '正常基准图片地址',
res_pos TEXT DEFAULT NULL COMMENT '识别位置',
filter VARCHAR(16) DEFAULT NULL COMMENT '0大模型 1初筛',
original_value LONGTEXT DEFAULT NULL COMMENT '原始值/纠偏值',
defect_level INT DEFAULT 0 COMMENT '0无 1重大 2严重 3一般 4轻微',
create_time DATETIME DEFAULT NULL COMMENT '分析结果创建时间',
synced_at DATETIME NOT NULL COMMENT '中心落库时间',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0存在 1对账删除',
PRIMARY KEY (id),
UNIQUE KEY uk_station_result_line (station_code, line_id),
KEY idx_station_result_time (station_code, create_time),
KEY idx_station_result_group (station_code, task_patrol_id(191), object_id(64), alg_type(64)),
KEY idx_station_result_deleted (station_code, is_deleted)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '巡检分析结果镜像表';
CREATE TABLE IF NOT EXISTS agg_inspection_report (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
line_id BIGINT NOT NULL COMMENT '源 line_id',
station_name VARCHAR(255) DEFAULT NULL COMMENT '变电站名称',
volt_level VARCHAR(255) DEFAULT NULL COMMENT '电压等级',
station_type VARCHAR(255) DEFAULT NULL COMMENT '变电站类型',
inspection_date DATETIME DEFAULT NULL COMMENT '巡视计划创建日期',
inspection_task_name VARCHAR(255) DEFAULT NULL COMMENT '巡视任务名称',
env_info TEXT DEFAULT NULL COMMENT '环境信息',
inspection_start_time DATETIME DEFAULT NULL COMMENT '巡视开始时间',
inspection_end_time DATETIME DEFAULT NULL COMMENT '巡视结束时间',
patrol_statistics VARCHAR(255) DEFAULT NULL COMMENT '巡视统计',
check_person VARCHAR(255) DEFAULT NULL COMMENT '审核人',
check_time DATETIME DEFAULT NULL COMMENT '审核时间',
description TEXT DEFAULT NULL COMMENT '巡视结论',
file_path TEXT DEFAULT NULL COMMENT '报告文件路径',
task_id TEXT DEFAULT NULL COMMENT '巡视任务 ID',
task_result_id VARCHAR(255) DEFAULT NULL COMMENT '任务结果 ID',
task_patrolled_id VARCHAR(255) DEFAULT NULL COMMENT '任务执行 ID',
location VARCHAR(255) DEFAULT NULL COMMENT '位置',
filter VARCHAR(16) DEFAULT NULL COMMENT '过滤标识',
synced_at DATETIME NOT NULL COMMENT '中心落库时间',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0存在 1对账删除',
PRIMARY KEY (id),
UNIQUE KEY uk_station_report_line (station_code, line_id),
KEY idx_station_report_date (station_code, inspection_date),
KEY idx_station_report_name (station_code, inspection_task_name),
KEY idx_station_report_deleted (station_code, is_deleted)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '巡视报告镜像表';
CREATE TABLE IF NOT EXISTS agg_analysis_defect_month (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT '站点编码',
stat_month DATE NOT NULL COMMENT '统计月份(当月1日)',
point_type VARCHAR(255) NOT NULL DEFAULT '' COMMENT '算法大类字典编码,空字符串为总览',
minor_count INT NOT NULL DEFAULT 0 COMMENT '轻微缺陷数量',
general_count INT NOT NULL DEFAULT 0 COMMENT '一般缺陷数量',
serious_count INT NOT NULL DEFAULT 0 COMMENT '严重缺陷数量',
urgent_count INT NOT NULL DEFAULT 0 COMMENT '重大缺陷数量',
normal_count INT NOT NULL DEFAULT 0 COMMENT '正常数量',
synced_at DATETIME NOT NULL COMMENT '同步时间',
PRIMARY KEY (id),
UNIQUE KEY uk_station_month_point (station_code, stat_month, point_type),
KEY idx_station_month (station_code, stat_month)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '站端缺陷月度汇总表';
CREATE TABLE IF NOT EXISTS agg_defect_record (
id BIGINT NOT NULL AUTO_INCREMENT,
station_code VARCHAR(32) NOT NULL COMMENT 'Station code',
device_name VARCHAR(255) DEFAULT NULL COMMENT 'Device name',
channel_name VARCHAR(255) DEFAULT NULL COMMENT 'Channel name',
channel_code VARCHAR(255) DEFAULT NULL COMMENT 'Channel code',
device_source VARCHAR(255) DEFAULT NULL COMMENT 'Device source',
point_name VARCHAR(255) DEFAULT NULL COMMENT 'Point name',
point_id VARCHAR(64) DEFAULT NULL COMMENT 'Point ID',
warn_status VARCHAR(16) DEFAULT NULL COMMENT 'Warn status',
alg_name VARCHAR(255) DEFAULT NULL COMMENT 'Algorithm name',
res_value VARCHAR(255) DEFAULT NULL COMMENT 'Recognition value',
img VARCHAR(255) DEFAULT NULL COMMENT 'Display image',
filter_desc VARCHAR(255) DEFAULT NULL COMMENT 'Filter description',
bright_desc VARCHAR(255) DEFAULT NULL COMMENT 'Bright model description',
img_analyse VARCHAR(255) DEFAULT NULL COMMENT 'Analysis image',
bright_img_analyse VARCHAR(255) DEFAULT NULL COMMENT 'Bright analysis image',
defect_desc TEXT DEFAULT NULL COMMENT 'Merged defect descriptions',
line_id TEXT DEFAULT NULL COMMENT 'Source line IDs, comma-separated',
patrol_time DATETIME DEFAULT NULL COMMENT 'Patrol time',
point_status VARCHAR(16) DEFAULT NULL COMMENT 'Point status',
img_type VARCHAR(32) DEFAULT NULL COMMENT 'Image type',
data_type VARCHAR(32) DEFAULT NULL COMMENT 'Data type',
filter_value VARCHAR(16) DEFAULT NULL COMMENT 'Filter value',
request_id VARCHAR(255) DEFAULT NULL COMMENT 'Request ID',
res_status VARCHAR(16) DEFAULT NULL COMMENT 'Result status',
bright_res_status VARCHAR(32) DEFAULT NULL COMMENT 'Bright result status',
area_name VARCHAR(255) DEFAULT NULL COMMENT 'Area name',
area_id VARCHAR(64) DEFAULT NULL COMMENT 'Area ID',
alg_type VARCHAR(64) NOT NULL COMMENT 'Algorithm type',
object_id VARCHAR(64) NOT NULL COMMENT 'Object ID',
image_normal_url_path VARCHAR(255) DEFAULT NULL COMMENT 'Normal image path',
file_path VARCHAR(255) DEFAULT NULL COMMENT 'File path',
suggestion VARCHAR(255) DEFAULT NULL COMMENT 'Suggestion',
original_value LONGTEXT DEFAULT NULL COMMENT 'Original value',
task_patrol_id VARCHAR(64) NOT NULL COMMENT 'Task patrol ID',
task_name VARCHAR(255) DEFAULT NULL COMMENT 'Task name',
defect_level INT DEFAULT 0 COMMENT 'Defect level',
synced_at DATETIME NOT NULL COMMENT 'Center sync time',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '0 active, 1 deleted by reconciliation',
PRIMARY KEY (id),
UNIQUE KEY uk_station_defect_group (station_code, task_patrol_id, object_id, alg_type),
KEY idx_station_patrol_time (station_code, patrol_time),
KEY idx_station_point_name (station_code, point_name),
KEY idx_station_deleted (station_code, is_deleted)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'Defect record snapshot from station API';

+ 54
- 0
inspect-main/inspect-main-agg/src/main/resources/mapper/AggAnalysisDefectMonthMapper.xml View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.inspect.agg.mapper.AggAnalysisDefectMonthMapper">
<resultMap id="BaseResultMap" type="com.inspect.agg.entity.AggAnalysisDefectMonth">
<id column="id" property="id"/>
<result column="station_code" property="stationCode"/>
<result column="agg_station_name" property="stationName"/>
<result column="stat_month" property="statMonth"/>
<result column="point_type" property="pointType"/>
<result column="minor_count" property="minorCount"/>
<result column="general_count" property="generalCount"/>
<result column="serious_count" property="seriousCount"/>
<result column="urgent_count" property="urgentCount"/>
<result column="normal_count" property="normalCount"/>
<result column="synced_at" property="syncedAt"/>
</resultMap>
<insert id="upsert" parameterType="com.inspect.agg.entity.AggAnalysisDefectMonth">
INSERT INTO agg_analysis_defect_month
(station_code, stat_month, point_type, minor_count, general_count,
serious_count, urgent_count, normal_count, synced_at)
VALUES
(#{stationCode}, #{statMonth}, #{pointType}, #{minorCount}, #{generalCount},
#{seriousCount}, #{urgentCount}, #{normalCount}, #{syncedAt})
ON DUPLICATE KEY UPDATE
minor_count = VALUES(minor_count),
general_count = VALUES(general_count),
serious_count = VALUES(serious_count),
urgent_count = VALUES(urgent_count),
normal_count = VALUES(normal_count),
synced_at = VALUES(synced_at)
</insert>
<delete id="deleteRange">
DELETE FROM agg_analysis_defect_month
WHERE station_code = #{stationCode}
AND stat_month &gt;= #{beginMonth}
AND stat_month &lt; #{endMonth}
</delete>
<select id="selectRange" resultMap="BaseResultMap">
SELECT m.id, m.station_code, m.stat_month, m.point_type, m.minor_count, m.general_count,
m.serious_count, m.urgent_count, m.normal_count, m.synced_at,
COALESCE(s.station_name, m.station_code) AS agg_station_name
FROM agg_analysis_defect_month m
LEFT JOIN agg_station s ON s.station_code = m.station_code
WHERE m.stat_month &gt;= #{beginMonth}
AND m.stat_month &lt; #{endMonth}
<if test="stationCode != null and stationCode != ''">
AND m.station_code = #{stationCode}
</if>
ORDER BY m.station_code, m.stat_month, m.point_type
</select>
<select id="countAll" resultType="long">
SELECT COUNT(*) FROM agg_analysis_defect_month
</select>
</mapper>

+ 81
- 0
inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataAreaMapper.xml View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.inspect.agg.mapper.AggBasedataAreaMapper">
<resultMap id="BaseResultMap" type="com.inspect.agg.entity.AggBasedataArea">
<id column="id" property="id"/>
<result column="station_code" property="stationCode"/>
<result column="agg_station_name" property="stationName"/>
<result column="area_id" property="areaId"/>
<result column="parent_id" property="parentId"/>
<result column="ancestors" property="ancestors"/>
<result column="area_name" property="areaName"/>
<result column="area_code" property="areaCode"/>
<result column="area_addr" property="areaAddr"/>
<result column="area_type" property="areaType"/>
<result column="device_type_code" property="deviceTypeCode"/>
<result column="order_num" property="orderNum"/>
<result column="status" property="status"/>
<result column="remark" property="remark"/>
<result column="update_time" property="updateTime"/>
<result column="synced_at" property="syncedAt"/>
<result column="is_deleted" property="isDeleted"/>
</resultMap>
<!-- 幂等 upsert:按 (station_code, area_id) -->
<insert id="upsert" parameterType="com.inspect.agg.entity.AggBasedataArea">
INSERT INTO agg_basedata_area (station_code, area_id, parent_id, ancestors, area_name, area_code, area_addr, area_type, device_type_code, order_num, status, remark, update_time, synced_at, is_deleted)
VALUES (#{stationCode}, #{areaId}, #{parentId}, #{ancestors}, #{areaName}, #{areaCode}, #{areaAddr}, #{areaType}, #{deviceTypeCode}, #{orderNum}, #{status}, #{remark}, #{updateTime}, #{syncedAt}, #{isDeleted})
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
ancestors = VALUES(ancestors),
area_name = VALUES(area_name),
area_code = VALUES(area_code),
area_addr = VALUES(area_addr),
area_type = VALUES(area_type),
device_type_code = VALUES(device_type_code),
order_num = VALUES(order_num),
status = VALUES(status),
remark = VALUES(remark),
update_time = VALUES(update_time),
synced_at = VALUES(synced_at),
is_deleted = VALUES(is_deleted),
is_deleted = 0
</insert>
<!-- 全量对账:本次未出现的行标记删除 -->
<update id="markDeletedExcept">
UPDATE agg_basedata_area
SET is_deleted = 1, synced_at = NOW()
WHERE station_code = #{stationCode} AND is_deleted = 0
<if test="ids != null and ids.size() > 0">
AND area_id NOT IN
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</if>
</update>
<select id="listByStation" resultMap="BaseResultMap">
SELECT a.*, COALESCE(s.station_name, a.station_code) AS agg_station_name
FROM agg_basedata_area a
LEFT JOIN agg_station s ON s.station_code = a.station_code
WHERE a.is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND a.station_code = #{stationCode}</if>
ORDER BY a.order_num, a.area_id
<if test="limit > 0">LIMIT #{limit}</if>
</select>
<select id="listForTree" resultMap="BaseResultMap">
SELECT a.*, COALESCE(s.station_name, a.station_code) AS agg_station_name
FROM agg_basedata_area a
LEFT JOIN agg_station s ON s.station_code = a.station_code
WHERE a.is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND a.station_code = #{stationCode}</if>
ORDER BY a.order_num, a.area_id
</select>
<select id="countByStation" resultType="long">
SELECT COUNT(*) FROM agg_basedata_area
WHERE is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND station_code = #{stationCode}</if>
</select>
</mapper>

+ 82
- 0
inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataDeviceMapper.xml View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.inspect.agg.mapper.AggBasedataDeviceMapper">
<resultMap id="BaseResultMap" type="com.inspect.agg.entity.AggBasedataDevice">
<id column="id" property="id"/>
<result column="station_code" property="stationCode"/>
<result column="agg_station_name" property="stationName"/>
<result column="device_id" property="deviceId"/>
<result column="parent_id" property="parentId"/>
<result column="ancestors" property="ancestors"/>
<result column="area_id" property="areaId"/>
<result column="area_name" property="areaName"/>
<result column="device_code" property="deviceCode"/>
<result column="device_name" property="deviceName"/>
<result column="device_type_name" property="deviceTypeName"/>
<result column="device_model" property="deviceModel"/>
<result column="manufacturer" property="manufacturer"/>
<result column="power_main_equ_flag" property="powerMainEquFlag"/>
<result column="station_no" property="stationNo"/>
<result column="monitor_index_no" property="monitorIndexNo"/>
<result column="status" property="status"/>
<result column="order_num" property="orderNum"/>
<result column="update_time" property="updateTime"/>
<result column="synced_at" property="syncedAt"/>
<result column="is_deleted" property="isDeleted"/>
</resultMap>
<!-- 幂等 upsert:按 (station_code, device_id) -->
<insert id="upsert" parameterType="com.inspect.agg.entity.AggBasedataDevice">
INSERT INTO agg_basedata_device (station_code, device_id, parent_id, ancestors, area_id, area_name, device_code, device_name, device_type_name, device_model, manufacturer, power_main_equ_flag, station_no, monitor_index_no, status, order_num, update_time, synced_at, is_deleted)
VALUES (#{stationCode}, #{deviceId}, #{parentId}, #{ancestors}, #{areaId}, #{areaName}, #{deviceCode}, #{deviceName}, #{deviceTypeName}, #{deviceModel}, #{manufacturer}, #{powerMainEquFlag}, #{stationNo}, #{monitorIndexNo}, #{status}, #{orderNum}, #{updateTime}, #{syncedAt}, #{isDeleted})
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
ancestors = VALUES(ancestors),
area_id = VALUES(area_id),
area_name = VALUES(area_name),
device_code = VALUES(device_code),
device_name = VALUES(device_name),
device_type_name = VALUES(device_type_name),
device_model = VALUES(device_model),
manufacturer = VALUES(manufacturer),
power_main_equ_flag = VALUES(power_main_equ_flag),
station_no = VALUES(station_no),
monitor_index_no = VALUES(monitor_index_no),
status = VALUES(status),
order_num = VALUES(order_num),
update_time = VALUES(update_time),
synced_at = VALUES(synced_at),
is_deleted = VALUES(is_deleted),
is_deleted = 0
</insert>
<!-- 全量对账:本次未出现的行标记删除 -->
<update id="markDeletedExcept">
UPDATE agg_basedata_device
SET is_deleted = 1, synced_at = NOW()
WHERE station_code = #{stationCode} AND is_deleted = 0
<if test="ids != null and ids.size() > 0">
AND device_id NOT IN
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</if>
</update>
<select id="list" resultMap="BaseResultMap">
SELECT d.*, COALESCE(s.station_name, d.station_code) AS agg_station_name
FROM agg_basedata_device d
LEFT JOIN agg_station s ON s.station_code = d.station_code
WHERE d.is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND d.station_code = #{stationCode}</if>
<if test="areaId != null">AND d.area_id = #{areaId}</if>
ORDER BY d.area_id, d.order_num, d.device_id
<if test="limit > 0">LIMIT #{limit}</if>
</select>
<select id="count" resultType="long">
SELECT COUNT(*) FROM agg_basedata_device
WHERE is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND station_code = #{stationCode}</if>
<if test="areaId != null">AND area_id = #{areaId}</if>
</select>
</mapper>

+ 76
- 0
inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataDevicetypeMapper.xml View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.inspect.agg.mapper.AggBasedataDevicetypeMapper">
<resultMap id="BaseResultMap" type="com.inspect.agg.entity.AggBasedataDevicetype">
<id column="id" property="id"/>
<result column="station_code" property="stationCode"/>
<result column="agg_station_name" property="stationName"/>
<result column="device_type_id" property="deviceTypeId"/>
<result column="parent_id" property="parentId"/>
<result column="order_num" property="orderNum"/>
<result column="ancestors" property="ancestors"/>
<result column="ancestors_addr" property="ancestorsAddr"/>
<result column="device_type_name" property="deviceTypeName"/>
<result column="device_type_code" property="deviceTypeCode"/>
<result column="power_main_equ_flag" property="powerMainEquFlag"/>
<result column="phase_num" property="phaseNum"/>
<result column="phase" property="phase"/>
<result column="status" property="status"/>
<result column="profession_type" property="professionType"/>
<result column="level" property="level"/>
<result column="update_time" property="updateTime"/>
<result column="synced_at" property="syncedAt"/>
<result column="is_deleted" property="isDeleted"/>
</resultMap>
<!-- 幂等 upsert:按 (station_code, device_type_id) -->
<insert id="upsert" parameterType="com.inspect.agg.entity.AggBasedataDevicetype">
INSERT INTO agg_basedata_devicetype (station_code, device_type_id, parent_id, order_num, ancestors, ancestors_addr, device_type_name, device_type_code, power_main_equ_flag, phase_num, phase, status, profession_type, level, update_time, synced_at, is_deleted)
VALUES (#{stationCode}, #{deviceTypeId}, #{parentId}, #{orderNum}, #{ancestors}, #{ancestorsAddr}, #{deviceTypeName}, #{deviceTypeCode}, #{powerMainEquFlag}, #{phaseNum}, #{phase}, #{status}, #{professionType}, #{level}, #{updateTime}, #{syncedAt}, #{isDeleted})
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
order_num = VALUES(order_num),
ancestors = VALUES(ancestors),
ancestors_addr = VALUES(ancestors_addr),
device_type_name = VALUES(device_type_name),
device_type_code = VALUES(device_type_code),
power_main_equ_flag = VALUES(power_main_equ_flag),
phase_num = VALUES(phase_num),
phase = VALUES(phase),
status = VALUES(status),
profession_type = VALUES(profession_type),
level = VALUES(level),
update_time = VALUES(update_time),
synced_at = VALUES(synced_at),
is_deleted = VALUES(is_deleted),
is_deleted = 0
</insert>
<!-- 全量对账:本次未出现的行标记删除 -->
<update id="markDeletedExcept">
UPDATE agg_basedata_devicetype
SET is_deleted = 1, synced_at = NOW()
WHERE station_code = #{stationCode} AND is_deleted = 0
<if test="ids != null and ids.size() > 0">
AND device_type_id NOT IN
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</if>
</update>
<select id="list" resultMap="BaseResultMap">
SELECT d.*, COALESCE(s.station_name, d.station_code) AS agg_station_name
FROM agg_basedata_devicetype d
LEFT JOIN agg_station s ON s.station_code = d.station_code
WHERE d.is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND d.station_code = #{stationCode}</if>
ORDER BY d.parent_id, d.order_num, d.device_type_id
<if test="limit > 0">LIMIT #{limit}</if>
</select>
<select id="count" resultType="long">
SELECT COUNT(*) FROM agg_basedata_devicetype
WHERE is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND station_code = #{stationCode}</if>
</select>
</mapper>

+ 87
- 0
inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataEqpbookChannelMapper.xml View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.inspect.agg.mapper.AggBasedataEqpbookChannelMapper">
<resultMap id="BaseResultMap" type="com.inspect.agg.entity.AggBasedataEqpbookChannel">
<id column="id" property="id"/>
<result column="station_code" property="stationCode"/>
<result column="agg_station_name" property="stationName"/>
<result column="channel_id" property="channelId"/>
<result column="channel_code" property="channelCode"/>
<result column="channel_name" property="channelName"/>
<result column="channel_type" property="channelType"/>
<result column="online_state" property="onlineState"/>
<result column="channel_content" property="channelContent"/>
<result column="host" property="host"/>
<result column="port" property="port"/>
<result column="area_id" property="areaId"/>
<result column="area_name" property="areaName"/>
<result column="eqpbook_id" property="eqpbookId"/>
<result column="patrol_device_code" property="patrolDeviceCode"/>
<result column="up_system_device_code" property="upSystemDeviceCode"/>
<result column="device_source" property="deviceSource"/>
<result column="update_time" property="updateTime"/>
<result column="synced_at" property="syncedAt"/>
<result column="is_deleted" property="isDeleted"/>
</resultMap>
<!-- 骞傜瓑 upsert锛氭寜 (station_code, channel_id) -->
<insert id="upsert" parameterType="com.inspect.agg.entity.AggBasedataEqpbookChannel">
INSERT INTO agg_basedata_eqpbook_channel (station_code, channel_id, channel_code, channel_name, channel_type, online_state, channel_content, host, port, area_id, area_name, eqpbook_id, patrol_device_code, up_system_device_code, device_source, update_time, synced_at, is_deleted)
VALUES (#{stationCode}, #{channelId}, #{channelCode}, #{channelName}, #{channelType}, #{onlineState}, #{channelContent}, #{host}, #{port}, #{areaId}, #{areaName}, #{eqpbookId}, #{patrolDeviceCode}, #{upSystemDeviceCode}, #{deviceSource}, #{updateTime}, #{syncedAt}, #{isDeleted})
ON DUPLICATE KEY UPDATE
channel_code = VALUES(channel_code),
channel_name = VALUES(channel_name),
channel_type = VALUES(channel_type),
online_state = VALUES(online_state),
channel_content = VALUES(channel_content),
host = VALUES(host),
port = VALUES(port),
area_id = VALUES(area_id),
area_name = VALUES(area_name),
eqpbook_id = VALUES(eqpbook_id),
patrol_device_code = VALUES(patrol_device_code),
up_system_device_code = VALUES(up_system_device_code),
device_source = VALUES(device_source),
update_time = VALUES(update_time),
synced_at = VALUES(synced_at),
is_deleted = VALUES(is_deleted),
is_deleted = 0
</insert>
<!-- 鍏ㄩ噺瀵硅处锛氭湰娆℃湭鍑虹幇鐨勮鏍囪鍒犻櫎 -->
<update id="markDeletedExcept">
UPDATE agg_basedata_eqpbook_channel
SET is_deleted = 1, synced_at = NOW()
WHERE station_code = #{stationCode} AND is_deleted = 0
<if test="ids != null and ids.size() > 0">
AND channel_id NOT IN
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</if>
</update>
<select id="list" resultMap="BaseResultMap">
SELECT c.*, COALESCE(s.station_name, c.station_code) AS agg_station_name
FROM agg_basedata_eqpbook_channel c
LEFT JOIN agg_station s ON s.station_code = c.station_code
WHERE c.is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND c.station_code = #{stationCode}</if>
ORDER BY c.eqpbook_id, c.channel_id
<if test="limit > 0">LIMIT #{limit}</if>
</select>
<select id="count" resultType="long">
SELECT COUNT(*) FROM agg_basedata_eqpbook_channel
WHERE is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND station_code = #{stationCode}</if>
</select>
<select id="countByDeviceSource" resultType="java.util.LinkedHashMap">
SELECT COALESCE(NULLIF(TRIM(device_source), ''), '其他') AS deviceSource,
COUNT(*) AS count
FROM agg_basedata_eqpbook_channel
WHERE is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND station_code = #{stationCode}</if>
GROUP BY COALESCE(NULLIF(TRIM(device_source), ''), '其他')
ORDER BY count DESC, deviceSource ASC
</select>
</mapper>

+ 81
- 0
inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataEqpbookMapper.xml View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.inspect.agg.mapper.AggBasedataEqpbookMapper">
<resultMap id="BaseResultMap" type="com.inspect.agg.entity.AggBasedataEqpbook">
<id column="id" property="id"/>
<result column="station_code" property="stationCode"/>
<result column="eqpbook_id" property="eqpbookId"/>
<result column="patrol_device_code" property="patrolDeviceCode"/>
<result column="patrol_device_name" property="patrolDeviceName"/>
<result column="agg_station_name" property="stationName"/>
<result column="area_id" property="areaId"/>
<result column="area_name" property="areaName"/>
<result column="device_source" property="deviceSource"/>
<result column="device_model" property="deviceModel"/>
<result column="manufacturer" property="manufacturer"/>
<result column="device_type" property="deviceType"/>
<result column="ip_addr" property="ipAddr"/>
<result column="port" property="port"/>
<result column="video_nvr_code" property="videoNvrCode"/>
<result column="main_system_code" property="mainSystemCode"/>
<result column="use_mode" property="useMode"/>
<result column="video_mode" property="videoMode"/>
<result column="update_time" property="updateTime"/>
<result column="synced_at" property="syncedAt"/>
<result column="is_deleted" property="isDeleted"/>
</resultMap>
<!-- 幂等 upsert:按 (station_code, eqpbook_id) -->
<insert id="upsert" parameterType="com.inspect.agg.entity.AggBasedataEqpbook">
INSERT INTO agg_basedata_eqpbook (station_code, eqpbook_id, patrol_device_code, patrol_device_name, station_name, area_id, area_name, device_source, device_model, manufacturer, device_type, ip_addr, port, video_nvr_code, main_system_code, use_mode, video_mode, update_time, synced_at, is_deleted)
VALUES (#{stationCode}, #{eqpbookId}, #{patrolDeviceCode}, #{patrolDeviceName}, #{stationName}, #{areaId}, #{areaName}, #{deviceSource}, #{deviceModel}, #{manufacturer}, #{deviceType}, #{ipAddr}, #{port}, #{videoNvrCode}, #{mainSystemCode}, #{useMode}, #{videoMode}, #{updateTime}, #{syncedAt}, #{isDeleted})
ON DUPLICATE KEY UPDATE
patrol_device_code = VALUES(patrol_device_code),
patrol_device_name = VALUES(patrol_device_name),
station_name = VALUES(station_name),
area_id = VALUES(area_id),
area_name = VALUES(area_name),
device_source = VALUES(device_source),
device_model = VALUES(device_model),
manufacturer = VALUES(manufacturer),
device_type = VALUES(device_type),
ip_addr = VALUES(ip_addr),
port = VALUES(port),
video_nvr_code = VALUES(video_nvr_code),
main_system_code = VALUES(main_system_code),
use_mode = VALUES(use_mode),
video_mode = VALUES(video_mode),
update_time = VALUES(update_time),
synced_at = VALUES(synced_at),
is_deleted = VALUES(is_deleted),
is_deleted = 0
</insert>
<!-- 全量对账:本次未出现的行标记删除 -->
<update id="markDeletedExcept">
UPDATE agg_basedata_eqpbook
SET is_deleted = 1, synced_at = NOW()
WHERE station_code = #{stationCode} AND is_deleted = 0
<if test="ids != null and ids.size() > 0">
AND eqpbook_id NOT IN
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</if>
</update>
<select id="list" resultMap="BaseResultMap">
SELECT e.*, COALESCE(s.station_name, NULLIF(e.station_name, ''), e.station_code) AS agg_station_name
FROM agg_basedata_eqpbook e
LEFT JOIN agg_station s ON s.station_code = e.station_code
WHERE e.is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND e.station_code = #{stationCode}</if>
ORDER BY e.area_id, e.eqpbook_id
<if test="limit > 0">LIMIT #{limit}</if>
</select>
<select id="count" resultType="long">
SELECT COUNT(*) FROM agg_basedata_eqpbook
WHERE is_deleted = 0
<if test="stationCode != null and stationCode != ''">AND station_code = #{stationCode}</if>
</select>
</mapper>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save