diff --git a/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/AlgTypeEnum.java b/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/AlgCategoryEnum.java similarity index 84% rename from inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/AlgTypeEnum.java rename to inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/AlgCategoryEnum.java index a46bf21..ac80780 100644 --- a/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/AlgTypeEnum.java +++ b/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/AlgCategoryEnum.java @@ -1,6 +1,6 @@ package com.inspect.base.core.enums; -public enum AlgTypeEnum { +public enum AlgCategoryEnum { // 表计 METER("1", "表计"), // 外观 @@ -10,7 +10,7 @@ public enum AlgTypeEnum { private final String code; private final String info; - AlgTypeEnum(String code, String info) { + AlgCategoryEnum(String code, String info) { this.code = code; this.info = info; } diff --git a/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/DefectLevel.java b/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/DefectLevel.java index 730bd45..6b846b7 100644 --- a/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/DefectLevel.java +++ b/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/DefectLevel.java @@ -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; diff --git a/inspect-ivs/src/main/java/com/inspect/ivs/controller/IvsStreamController.java b/inspect-ivs/src/main/java/com/inspect/ivs/controller/IvsStreamController.java index bf563a0..ac697c5 100644 --- a/inspect-ivs/src/main/java/com/inspect/ivs/controller/IvsStreamController.java +++ b/inspect-ivs/src/main/java/com/inspect/ivs/controller/IvsStreamController.java @@ -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( diff --git a/inspect-main/inspect-main-agg/pom.xml b/inspect-main/inspect-main-agg/pom.xml new file mode 100644 index 0000000..3fb006b --- /dev/null +++ b/inspect-main/inspect-main-agg/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + com.inspect + inspect-main + 3.4.0 + + inspect-main-agg + 3.4.0 + inspect-main-agg + inspect-main-agg + + 1.8 + UTF-8 + UTF-8 + 2.6.13 + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web + + + com.inspect + inspect-base-core + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + org.springframework.boot + spring-boot-dependencies + 2.6.6 + pom + import + + + + diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/AggregationApplication.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/AggregationApplication.java new file mode 100644 index 0000000..5070f9d --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/AggregationApplication.java @@ -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); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggCalendarEvent.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggCalendarEvent.java new file mode 100644 index 0000000..4fa9f21 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggCalendarEvent.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggTaskCalendarGenerator.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggTaskCalendarGenerator.java new file mode 100644 index 0000000..9b280a2 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggTaskCalendarGenerator.java @@ -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 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 events = new ArrayList<>(); + List 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 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 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 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 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); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggTaskCalendarService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggTaskCalendarService.java new file mode 100644 index 0000000..fa1cf95 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/calendar/AggTaskCalendarService.java @@ -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> EVENT_LIST_TYPE = + new TypeReference>() { }; + private static final TypeReference>> STATS_LIST_TYPE = + new TypeReference>>() { }; + + private final AggTaskCalendarGenerator calendarGenerator; + private final AggPatrolTaskStatusMapper taskStatusMapper; + private final StationRegistry stationRegistry; + private final StringRedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + public List> year(String stationCode, int year) { + String scope = scope(stationCode); + String cacheKey = CACHE_PREFIX + "summary:" + scope + ":" + year; + List> cached = readCache(cacheKey, STATS_LIST_TYPE); + if (cached != null) { + return cached; + } + + List> result = new ArrayList<>(); + for (int month = 1; month <= 12; month++) { + Map 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 events = visibleEvents(loadMonthEvents(stationCode, year, month)); + Map 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> month(String stationCode, int year, int month) { + List events = visibleEvents(loadMonthEvents(stationCode, year, month)); + applyStationNames(events); + return toMaps(events); + } + + public List> day(String stationCode, int year, int month, int day) { + LocalDate target = LocalDate.of(year, month, day); + List 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 loadMonthEvents(String stationCode, int year, int month) { + String scope = scope(stationCode); + String key = monthKey(stationCode, year, month); + List cached = readCache(key, EVENT_LIST_TYPE); + if (cached != null) { + return cached; + } + + List 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 loadStationMonthCached(String stationCode, int year, int month) { + String key = monthKey(stationCode, year, month); + List cached = readCache(key, EVENT_LIST_TYPE); + if (cached != null) { + return cached; + } + List result = loadStationMonth(stationCode, year, month); + writeCache(key, result, monthTtl(year, month)); + return result; + } + + private List loadStationMonth(String stationCode, int year, int month) { + List events = new ArrayList<>(); + events.addAll(calendarGenerator.generateMonth(stationCode, year, month)); + List 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 merge(List events) { + Map 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 result = new ArrayList<>(unique.values()); + result.sort((a, b) -> a.getStartTime().compareTo(b.getStartTime())); + return result; + } + + private List visibleEvents(List events) { + LocalDateTime now = LocalDateTime.now(); + List 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> toMaps(List events) { + List> result = new ArrayList<>(); + for (AggCalendarEvent event : events) { + Map 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 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 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 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 readCache(String key, TypeReference 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)); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/AggProperties.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/AggProperties.java new file mode 100644 index 0000000..f19b474 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/AggProperties.java @@ -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 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/RestTemplateConfig.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/RestTemplateConfig.java new file mode 100644 index 0000000..6686efe --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/RestTemplateConfig.java @@ -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> converters = restTemplate.getMessageConverters(); + for (HttpMessageConverter converter : converters) { + if (converter instanceof StringHttpMessageConverter) { + StringHttpMessageConverter stringConverter = (StringHttpMessageConverter) converter; + stringConverter.setDefaultCharset(StandardCharsets.UTF_8); + } + } + return restTemplate; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/SyncDefaults.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/SyncDefaults.java new file mode 100644 index 0000000..e54f405 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/config/SyncDefaults.java @@ -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() { + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/dto/AggMonitorPatrolPointVo.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/dto/AggMonitorPatrolPointVo.java new file mode 100644 index 0000000..755dac0 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/dto/AggMonitorPatrolPointVo.java @@ -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; +} diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/dto/VideoRequest.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/dto/VideoRequest.java new file mode 100644 index 0000000..09c7ccd --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/dto/VideoRequest.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggAlgSubtype.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggAlgSubtype.java new file mode 100644 index 0000000..d858a7e --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggAlgSubtype.java @@ -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; +} diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggAnalysisDefectMonth.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggAnalysisDefectMonth.java new file mode 100644 index 0000000..a834c33 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggAnalysisDefectMonth.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataArea.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataArea.java new file mode 100644 index 0000000..1781a0c --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataArea.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataDevice.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataDevice.java new file mode 100644 index 0000000..47071a1 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataDevice.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataDevicetype.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataDevicetype.java new file mode 100644 index 0000000..b2b36c2 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataDevicetype.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataEqpbook.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataEqpbook.java new file mode 100644 index 0000000..4aa81b6 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataEqpbook.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataEqpbookChannel.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataEqpbookChannel.java new file mode 100644 index 0000000..319cf7d --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataEqpbookChannel.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataPatrolpoint.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataPatrolpoint.java new file mode 100644 index 0000000..38555af --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggBasedataPatrolpoint.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggDefectRecord.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggDefectRecord.java new file mode 100644 index 0000000..831f3ee --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggDefectRecord.java @@ -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; +} diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggInspectionReport.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggInspectionReport.java new file mode 100644 index 0000000..15b7502 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggInspectionReport.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggPatrolTask.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggPatrolTask.java new file mode 100644 index 0000000..4f8ba10 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggPatrolTask.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggPatrolTaskStatus.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggPatrolTaskStatus.java new file mode 100644 index 0000000..3f4328b --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggPatrolTaskStatus.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggResultAnalysis.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggResultAnalysis.java new file mode 100644 index 0000000..a1ec8aa --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggResultAnalysis.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggStation.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggStation.java new file mode 100644 index 0000000..5b292ee --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggStation.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggSyncLog.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggSyncLog.java new file mode 100644 index 0000000..afdeeb2 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/entity/AggSyncLog.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/enums/AlgCategory.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/enums/AlgCategory.java new file mode 100644 index 0000000..b486c4c --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/enums/AlgCategory.java @@ -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); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggAnalysisDefectMonthMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggAnalysisDefectMonthMapper.java new file mode 100644 index 0000000..299b8f1 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggAnalysisDefectMonthMapper.java @@ -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 selectRange(@Param("stationCode") String stationCode, + @Param("beginMonth") LocalDate beginMonth, + @Param("endMonth") LocalDate endMonth); + + long countAll(); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataAreaMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataAreaMapper.java new file mode 100644 index 0000000..9625c75 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataAreaMapper.java @@ -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 ids); + + /** 列表(限制条数,limit<=0 表示不限制) */ + List listByStation(@Param("stationCode") String stationCode, @Param("limit") int limit); + + /** 整站区域(用于组树) */ + List listForTree(@Param("stationCode") String stationCode); + + long countByStation(@Param("stationCode") String stationCode); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataDeviceMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataDeviceMapper.java new file mode 100644 index 0000000..619c4ac --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataDeviceMapper.java @@ -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 ids); + + List list(@Param("stationCode") String stationCode, @Param("areaId") Long areaId, @Param("limit") int limit); + + long count(@Param("stationCode") String stationCode, @Param("areaId") Long areaId); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataDevicetypeMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataDevicetypeMapper.java new file mode 100644 index 0000000..35bc8eb --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataDevicetypeMapper.java @@ -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 ids); + + List list(@Param("stationCode") String stationCode, @Param("limit") int limit); + + long count(@Param("stationCode") String stationCode); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataEqpbookChannelMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataEqpbookChannelMapper.java new file mode 100644 index 0000000..a4fcd5b --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataEqpbookChannelMapper.java @@ -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 ids); + + List list(@Param("stationCode") String stationCode, @Param("limit") int limit); + + long count(@Param("stationCode") String stationCode); + + List> countByDeviceSource(@Param("stationCode") String stationCode); +} diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataEqpbookMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataEqpbookMapper.java new file mode 100644 index 0000000..5a09f7a --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataEqpbookMapper.java @@ -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 ids); + + List list(@Param("stationCode") String stationCode, @Param("limit") int limit); + + long count(@Param("stationCode") String stationCode); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataPatrolpointMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataPatrolpointMapper.java new file mode 100644 index 0000000..7df47db --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggBasedataPatrolpointMapper.java @@ -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 ids); + + List 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 selectMonitorRelationList(@Param("stationCode") String stationCode, + @Param("deviceId") Long deviceId); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggDefectRecordMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggDefectRecordMapper.java new file mode 100644 index 0000000..2bdb229 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggDefectRecordMapper.java @@ -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 selectActiveKeys(@Param("stationCode") String stationCode); + + int markDeletedByIds(@Param("stationCode") String stationCode, @Param("ids") List ids); + + List selectList(@Param("stationCode") String stationCode, + @Param("pointName") String pointName, + @Param("patrolTimeBegin") String patrolTimeBegin, + @Param("patrolTimeEnd") String patrolTimeEnd, + @Param("algName") String algName); + + List selectDistinctAlgNames(@Param("stationCode") String stationCode); +} diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggInspectionReportMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggInspectionReportMapper.java new file mode 100644 index 0000000..0e56fb9 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggInspectionReportMapper.java @@ -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 lineIds); + + List 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); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggPatrolTaskMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggPatrolTaskMapper.java new file mode 100644 index 0000000..da332a5 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggPatrolTaskMapper.java @@ -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 ids); + + List list(@Param("stationCode") String stationCode, @Param("limit") int limit); + + long count(@Param("stationCode") String stationCode); + + + List selectTaskList(@Param("stationCode") String stationCode, + @Param("taskName") String taskName); + List selectEnabledTasks(@Param("stationCode") String stationCode); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggPatrolTaskStatusMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggPatrolTaskStatusMapper.java new file mode 100644 index 0000000..cdf21e7 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggPatrolTaskStatusMapper.java @@ -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 ids); + + List list(@Param("stationCode") String stationCode, + @Param("taskState") String taskState, + @Param("limit") int limit); + + long count(@Param("stationCode") String stationCode, @Param("taskState") String taskState); + + List selectTaskStatusList(@Param("stationCode") String stationCode, + @Param("taskName") String taskName, + @Param("beginTime") String beginTime, + @Param("endTime") String endTime); + + List> countMonthlyStatsByYear(@Param("stationCode") String stationCode, + @Param("year") Integer year); + + List selectDailyStatsByYearAndMonth(@Param("stationCode") String stationCode, + @Param("year") Integer year, + @Param("month") Integer month); + + List selectDailyStatsByYearAndMonthAndDay(@Param("stationCode") String stationCode, + @Param("year") Integer year, + @Param("month") Integer month, + @Param("day") Integer day); + + Map selectInspectionTaskStats(@Param("stationCode") String stationCode, + @Param("pointTypeCode") String pointTypeCode, + @Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggResultAnalysisMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggResultAnalysisMapper.java new file mode 100644 index 0000000..b876b94 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggResultAnalysisMapper.java @@ -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> selectDefectStats(@Param("stationCode") String stationCode, + @Param("pointTypeCode") String pointTypeCode, + @Param("dateType") String dateType, + @Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggStationMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggStationMapper.java new file mode 100644 index 0000000..7ec9252 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggStationMapper.java @@ -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 selectEnabled(); + + List selectAllOrdered(); + + AggStation selectByCode(@Param("stationCode") String stationCode); + + List selectByEnabled(@Param("enabled") Integer enabled); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggSyncLogMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggSyncLogMapper.java new file mode 100644 index 0000000..266141a --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/AggSyncLogMapper.java @@ -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); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/BasedataAlgSubtypeMapper.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/BasedataAlgSubtypeMapper.java new file mode 100644 index 0000000..9bc584b --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/mapper/BasedataAlgSubtypeMapper.java @@ -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 selectEnabledList(); +} diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/BaseSyncScheduler.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/BaseSyncScheduler.java new file mode 100644 index 0000000..032a37f --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/BaseSyncScheduler.java @@ -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(); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/DefectRecordSyncScheduler.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/DefectRecordSyncScheduler.java new file mode 100644 index 0000000..20ac8b4 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/DefectRecordSyncScheduler.java @@ -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(); + } +} diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/DynamicSyncScheduler.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/DynamicSyncScheduler.java new file mode 100644 index 0000000..20b3731 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/DynamicSyncScheduler.java @@ -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(); + } + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/RealtimeViewerCleanupScheduler.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/RealtimeViewerCleanupScheduler.java new file mode 100644 index 0000000..2b33b5b --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/RealtimeViewerCleanupScheduler.java @@ -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); + } + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/ResultAnalysisSyncScheduler.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/ResultAnalysisSyncScheduler.java new file mode 100644 index 0000000..04a944a --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/scheduler/ResultAnalysisSyncScheduler.java @@ -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(); + } + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggAnalysisDefectMonthSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggAnalysisDefectMonthSyncService.java new file mode 100644 index 0000000..9e1674a --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggAnalysisDefectMonthSyncService.java @@ -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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggDeviceTreeService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggDeviceTreeService.java new file mode 100644 index 0000000..6b390d9 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggDeviceTreeService.java @@ -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 loadRelationTree(String stationCode, Map params) { + List stations; + if (StringUtils.hasText(stationCode)) { + stations = Collections.singletonList(stationRegistry.requireByCode(stationCode)); + } else { + stations = stationRegistry.listEnabled(); + } + boolean aggregate = !StringUtils.hasText(stationCode); + List result = new ArrayList<>(); + int pageSize = props.getSync().getBase().getPageSize(); + for (AggStation station : stations) { + List 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(); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggStatisticService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggStatisticService.java new file mode 100644 index 0000000..86b5ed7 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AggStatisticService.java @@ -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> 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 rows = defectMonthMapper.selectRange(stationCode, begin, endExclusive); + Map monthlyMap = toMonthlyMap(rows, category == null ? "" : category.getCode()); + + String stationName = stationDisplayName(stationCode); + List> 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> 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> result = new ArrayList<>(); + for (AlgCategory item : AlgCategory.values()) { + result.add(inspectionByCategory(stationCode, stationName, item)); + } + return result; + } + + private Map inspectionByCategory(String stationCode, String stationName, + AlgCategory category) { + YearMonth current = YearMonth.now(); + LocalDate begin = current.atDay(1); + LocalDate endExclusive = current.plusMonths(1).atDay(1); + Map taskStats = taskStatusMapper.selectInspectionTaskStats( + stationCode, category.getCode(), begin.atStartOfDay(), endExclusive.atStartOfDay()); + List 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 toMonthlyMap(List rows, String pointType) { + Map 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 toPeriodResult(Counts counts, String period, + String dateType, String algCategory, + String stationCode, String stationName) { + Map 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 toInspectionResult(String label, Map taskStats, + Counts defectStats, + String stationCode, String stationName) { + Map 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 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; + } + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AreaSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AreaSyncService.java new file mode 100644 index 0000000..7b1d38d --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/AreaSyncService.java @@ -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 rows = stationApiClient.fetchAllAreas(station, + props.getSync().getBase().getPageSize(), 500); + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/ChannelSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/ChannelSyncService.java new file mode 100644 index 0000000..1c536a1 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/ChannelSyncService.java @@ -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 rows = stationApiClient.fetchAllChannels(station, + props.getSync().getBase().getPageSize(), 100); + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DefectRecordSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DefectRecordSyncService.java new file mode 100644 index 0000000..cee1508 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DefectRecordSyncService.java @@ -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 existing = defectRecordMapper.selectActiveKeys(station.getStationCode()); + Map existingIds = existing.stream() + .collect(Collectors.toMap(this::naturalKey, AggDefectRecord::getId, (oldValue, ignored) -> oldValue)); + List rows = stationApiClient.fetchAllRows(station, PATH, + props.getSync().getBase().getPageSize(), 1000, "lineId"); + Set 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 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 query = new HashMap<>(); + query.put("patrolTimeBegin", LocalDate.now().minusDays(7).toString()); + query.put("patrolTimeEnd", LocalDate.now().toString()); + List 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(); + } +} diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DeviceSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DeviceSyncService.java new file mode 100644 index 0000000..33cc38e --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DeviceSyncService.java @@ -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 rows = stationApiClient.fetchAllDevices(station, + props.getSync().getBase().getPageSize(), 500); + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DevicetypeSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DevicetypeSyncService.java new file mode 100644 index 0000000..9a67552 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/DevicetypeSyncService.java @@ -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 rows = stationApiClient.fetchAllDeviceTypes(station, + props.getSync().getBase().getPageSize(), 100); + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/EqpbookSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/EqpbookSyncService.java new file mode 100644 index 0000000..d7ae034 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/EqpbookSyncService.java @@ -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 rows = stationApiClient.fetchAllEqpbooks(station, + props.getSync().getBase().getPageSize(), 100); + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/EqpbookTreeService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/EqpbookTreeService.java new file mode 100644 index 0000000..0243f91 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/EqpbookTreeService.java @@ -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> buildFilterTree(String stationCode, + String patrolDeviceName, + String onlineStatus, + String patrolDeviceTypeFlag) { + List areas = areaMapper.listForTree(stationCode); + List eqpbooks = eqpbookMapper.list(stationCode, 0); + List channels = channelMapper.list(stationCode, 0); + + Map areaIndex = new HashMap<>(); + for (AggBasedataArea area : areas) { + areaIndex.put(areaKey(area.getStationCode(), area.getAreaId()), area); + } + + Map> channelIndex = new HashMap<>(); + for (AggBasedataEqpbookChannel channel : channels) { + channelIndex.computeIfAbsent(eqpbookKey(channel.getStationCode(), channel.getEqpbookId()), + key -> new ArrayList<>()).add(channel); + } + + List selectedEqpbooks = new ArrayList<>(); + Set selectedAreaKeys = new HashSet<>(); + for (AggBasedataEqpbook eqpbook : eqpbooks) { + if (!matchesName(eqpbook, patrolDeviceName) || !matchesType(eqpbook, patrolDeviceTypeFlag)) { + continue; + } + List eqpbookChannels = + channelIndex.getOrDefault(eqpbookKey(eqpbook.getStationCode(), eqpbook.getEqpbookId()), + new ArrayList()); + 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> areaNodes = new LinkedHashMap<>(); + for (String key : selectedAreaKeys) { + AggBasedataArea area = areaIndex.get(key); + if (area == null) continue; + Map 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>()); + areaNodes.put(key, node); + } + + for (AggBasedataEqpbook eqpbook : selectedEqpbooks) { + String parentKey = areaKey(eqpbook.getStationCode(), eqpbook.getAreaId()); + Map areaNode = areaNodes.get(parentKey); + if (areaNode == null) continue; + List 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 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>()); + children(areaNode).add(eqpbookNode); + + for (AggBasedataEqpbookChannel channel : eqpbookChannels) { + Map 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> roots = new ArrayList<>(); + for (Map node : areaNodes.values()) { + if (node.get("parentId") == null) { + roots.add(node); + } else { + Map parent = areaNodes.get(node.get("parentId")); + if (parent != null) { + children(parent).add(node); + } else { + roots.add(node); + } + } + } + return roots; + } + + @SuppressWarnings("unchecked") + private static List> children(Map node) { + return (List>) node.get("children"); + } + + private static void collectAreaKeys(AggBasedataArea area, + Map areaIndex, + Set 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/InspectionReportSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/InspectionReportSyncService.java new file mode 100644 index 0000000..2b1113b --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/InspectionReportSyncService.java @@ -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 rows = stationApiClient.fetchAllInspectionReports( + station, props.getSync().getBase().getPageSize(), 100); + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/PatrolTaskSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/PatrolTaskSyncService.java new file mode 100644 index 0000000..3046a36 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/PatrolTaskSyncService.java @@ -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 rows = stationApiClient.fetchAllRows(station, "patrol/task/list", + props.getSync().getBase().getPageSize(), 100, "taskId"); + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/PointSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/PointSyncService.java new file mode 100644 index 0000000..740f049 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/PointSyncService.java @@ -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 rows = stationApiClient.fetchAllPatrolPoints(station, + props.getSync().getBase().getPageSize(), 500); + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/ResultAnalysisSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/ResultAnalysisSyncService.java new file mode 100644 index 0000000..e318428 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/ResultAnalysisSyncService.java @@ -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 query = new HashMap<>(); + query.put("beginTime", begin.format(DATE_TIME)); + query.put("endTime", end.minusSeconds(1).format(DATE_TIME)); + List 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(); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/StationRegistry.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/StationRegistry.java new file mode 100644 index 0000000..c71cc45 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/StationRegistry.java @@ -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 listEnabled() { + List list = aggStationMapper.selectEnabled(); + log.info("listEnabled: {}", list); + return list; + } + + /** 全部站点(含停用) */ + public List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/TaskExecSyncService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/TaskExecSyncService.java new file mode 100644 index 0000000..914bc00 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/service/TaskExecSyncService.java @@ -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 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 rows = stationApiClient.fetchAllRows(station, "patrol/taskstatus/list", + 500, 100, "lineId"); + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/station/StationApiClient.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/station/StationApiClient.java new file mode 100644 index 0000000..684a2af --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/station/StationApiClient.java @@ -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 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 fetchAllAreas(AggStation station, int pageSize, int maxPages) { + return fetchAllRows(station, "basedata/area/list", pageSize, maxPages, "areaId"); + } + + public List fetchAllInspectionReports(AggStation station, int pageSize, int maxPages) { + return fetchAllRows(station, "patrol/insreport/list", pageSize, maxPages, "lineId"); + } + + public List fetchAllDevices(AggStation station, int pageSize, int maxPages) { + return fetchAllRows(station, "basedata/prodevmnt/device/list", pageSize, maxPages, "deviceId"); + } + + public List fetchAllPatrolPoints(AggStation station, int pageSize, int maxPages) { + return fetchAllRows(station, "basedata/patrolpointmnt/patrolpoint/list", pageSize, maxPages, "patrolpointId"); + } + + public List fetchAllEqpbooks(AggStation station, int pageSize, int maxPages) { + return fetchAllRows(station, "basedata/eqpbook/list", pageSize, maxPages, "eqpbookId"); + } + + public List fetchAllChannels(AggStation station, int pageSize, int maxPages) { + return fetchAllRows(station, "basedata/eqpbook/channel/list", pageSize, maxPages, "channelId"); + } + + public List 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 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 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 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 body = new LinkedHashMap<>(); + body.put("cameraCode", cameraCode); + body.put("action", action); + return doPostJson(url, body, station); + } + + /** 通用分页拉取:按 total 或页大小/重复检测终止 */ + public List fetchAllRows(AggStation station, String path, int pageSize, int maxPages, String... idKeys) { + return fetchAllRows(station, path, pageSize, maxPages, Collections.emptyMap(), idKeys); + } + + /** 带附加查询参数(如 taskState/fileStatus)的分页拉取 */ + public List fetchAllRows(AggStation station, String path, int pageSize, int maxPages, + Map extraQuery, String... idKeys) { + String token = ensureToken(station); + List rows = new ArrayList<>(); + Set 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 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 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 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 rows; + private final long total; + + PageData(List rows, long total) { + this.rows = rows; + this.total = total; + } + + List rows() { + return rows; + } + + long total() { + return total; + } + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/station/TokenData.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/station/TokenData.java new file mode 100644 index 0000000..e4aea90 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/station/TokenData.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/support/Rows.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/support/Rows.java new file mode 100644 index 0000000..8add2f4 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/support/Rows.java @@ -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); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/support/SyncLogWriter.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/support/SyncLogWriter.java new file mode 100644 index 0000000..7a823d5 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/support/SyncLogWriter.java @@ -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()); + } + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/PlaybackSession.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/PlaybackSession.java new file mode 100644 index 0000000..24ed417 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/PlaybackSession.java @@ -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(); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/PlaybackSessionRegistry.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/PlaybackSessionRegistry.java new file mode 100644 index 0000000..7bc1564 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/PlaybackSessionRegistry.java @@ -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> 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 list = current == null ? new ArrayList<>() : current; + list.removeIf(item -> sessionId.equals(item.getSessionId())); + list.add(0, session); + return list; + }); + } + + public List list(String stationCode, String cameraCode) { + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamBinding.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamBinding.java new file mode 100644 index 0000000..4f20379 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamBinding.java @@ -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(); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamRegistry.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamRegistry.java new file mode 100644 index 0000000..9c42985 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamRegistry.java @@ -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 byCamera = new ConcurrentHashMap<>(); + private final Map 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamService.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamService.java new file mode 100644 index 0000000..6c68957 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeStreamService.java @@ -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 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 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; + } + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeViewerRegistry.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeViewerRegistry.java new file mode 100644 index 0000000..4f99904 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/RealtimeViewerRegistry.java @@ -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> tickets = new ConcurrentHashMap<>(); + private final Map lastActivity = new ConcurrentHashMap<>(); + + public void add(String streamId) { + tickets.compute(streamId, (key, current) -> { + List 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 current = tickets.get(streamId); + if (current == null) { + return 0; + } + synchronized (current) { + return current.size(); + } + } + + /** + * 清理过期票据,返回因此没有观看者的 streamId 列表。 + */ + public List expire(long now) { + List 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/StationStream.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/StationStream.java new file mode 100644 index 0000000..29cec40 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/StationStream.java @@ -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(); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/StationStreamRegistry.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/StationStreamRegistry.java new file mode 100644 index 0000000..64d99da --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/StationStreamRegistry.java @@ -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 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)); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/ZlmApiClient.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/ZlmApiClient.java new file mode 100644 index 0000000..2c322d5 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/video/ZlmApiClient.java @@ -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 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()); + } + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggAreaController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggAreaController.java new file mode 100644 index 0000000..910af74 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggAreaController.java @@ -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 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 m = new HashMap<>(); + m.put("code", 200); + m.put("msg", "sync triggered: " + entity); + return m; + } + + @GetMapping("/area/list") + public Map 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 areaTree(@RequestParam(required = false) String stationCode, + @RequestParam(required = false) Long rootId) { + List all = areaMapper.listForTree(stationCode); + Set ids = all.stream().map(AggBasedataArea::getAreaId).collect(Collectors.toSet()); + Map> byParent = all.stream() + .collect(Collectors.groupingBy(a -> a.getParentId() == null ? 0L : a.getParentId())); + List> 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 m = ok(roots, all.size()); + return m; + } + + @GetMapping("/device/list") + public Map 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 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 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 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 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 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 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 ok(Object data, long total) { + Map m = new HashMap<>(); + m.put("code", 200); + m.put("total", total); + m.put("data", data); + return m; + } + + private Map toNode(AggBasedataArea a, Map> byParent) { + Map 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> children = new ArrayList<>(); + for (AggBasedataArea c : byParent.getOrDefault(a.getAreaId(), Collections.emptyList())) { + children.add(toNode(c, byParent)); + } + node.put("children", children); + return node; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggCalendarController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggCalendarController.java new file mode 100644 index 0000000..9d16d50 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggCalendarController.java @@ -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> year(@RequestParam Integer year, + @RequestParam(required = false) String stationCode) { + checkYear(year); + return calendarService.year(stationCode, year); + } + + @GetMapping("/month") + public List> 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> 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("无效的月份"); + } + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggDefectRecordController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggDefectRecordController.java new file mode 100644 index 0000000..3f05510 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggDefectRecordController.java @@ -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 list = defectRecordMapper.selectList( + stationCode, pointName, patrolTimeBegin, patrolTimeEnd, algName); + PageInfo pageInfo = new PageInfo<>(list); + TableDataInfo result = new TableDataInfo(); + result.setCode(200); + result.setMsg("Query Ok"); + result.setRows(list); + result.setTotal(pageInfo.getTotal()); + return result; + } +} diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggDeviceTreeController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggDeviceTreeController.java new file mode 100644 index 0000000..4afe911 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggDeviceTreeController.java @@ -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 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)); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggInspectionReportController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggInspectionReportController.java new file mode 100644 index 0000000..d8442b0 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggInspectionReportController.java @@ -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 list = reportMapper.selectList( + stationCode, cycleStartTime, cycleEndTime, inspectionTaskName); + PageInfo 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolPointQueryController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolPointQueryController.java new file mode 100644 index 0000000..f6c7d05 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolPointQueryController.java @@ -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 list = patrolPointMapper.selectMonitorRelationList(stationCode, deviceId); + fillAlgSubtypeName(list); + PageInfo pageInfo = new PageInfo<>(list); + return dataTable(list, pageInfo.getTotal()); + } + + private void fillAlgSubtypeName(List list) { + if (list == null || list.isEmpty()) { + return; + } + Map 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; + } +} diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolTaskController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolTaskController.java new file mode 100644 index 0000000..394da3c --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolTaskController.java @@ -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 list = taskMapper.selectTaskList(stationCode, taskName); + PageInfo pageInfo = new PageInfo<>(list); + + TableDataInfo result = new TableDataInfo(); + result.setCode(200); + result.setMsg("Query Ok"); + result.setRows(list); + result.setTotal(pageInfo.getTotal()); + return result; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolTaskStatusController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolTaskStatusController.java new file mode 100644 index 0000000..ae81438 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggPatrolTaskStatusController.java @@ -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 list = taskStatusMapper.selectTaskStatusList( + stationCode, taskName, beginTime, endTime); + PageInfo pageInfo = new PageInfo<>(list); + + TableDataInfo result = new TableDataInfo(); + result.setCode(200); + result.setMsg("Query Ok"); + result.setRows(list); + result.setTotal(pageInfo.getTotal()); + return result; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggStationController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggStationController.java new file mode 100644 index 0000000..d806daa --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggStationController.java @@ -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 stations = stationMapper.selectByEnabled(enabled); + return AjaxResult.success(stations); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggStatisticController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggStatisticController.java new file mode 100644 index 0000000..11a7faa --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggStatisticController.java @@ -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> 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> items = channelMapper.countByDeviceSource(stationCode); + long totalCount = 0L; + for (Map item : items) { + Object count = item.get("count"); + if (count instanceof Number) { + totalCount += ((Number) count).longValue(); + } + } + + Map data = new LinkedHashMap<>(); + data.put("totalCount", totalCount); + data.put("deviceSourceCounts", items); + return AjaxResult.success(data); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggTreeController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggTreeController.java new file mode 100644 index 0000000..fa1987a --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggTreeController.java @@ -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> tree = eqpbookTreeService.buildFilterTree( + stationCode, patrolDeviceName, onlineStatus, patrolDeviceTypeFlag); + return AjaxResult.success(tree); + } + + @GetMapping("/device") + public AjaxResult treeAreaDeviceSelectFilterByPresetRelation( + @RequestParam(required = false) String stationCode, + HttpServletRequest request) { + Map 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)); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggVideoController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggVideoController.java new file mode 100644 index 0000000..c0c8338 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/AggVideoController.java @@ -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 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 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 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 sessions = playbackSessionRegistry.list(stationCode, cameraCode); + if (sessions.isEmpty()) { + return AjaxResult.error(404, "未找到可停止的历史回放会话"); + } + int stationStopped = 0; + int streamStopped = 0; + List 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 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; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/StationProxyController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/StationProxyController.java new file mode 100644 index 0000000..4c8b161 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/StationProxyController.java @@ -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 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 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 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 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("/+$", ""); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/VideoProxyController.java b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/VideoProxyController.java new file mode 100644 index 0000000..7f7cd12 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/java/com/inspect/agg/web/VideoProxyController.java @@ -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 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 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 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 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 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 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 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 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 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 hlsAlternatives(String hlsUrl) { + java.util.List 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 error(int code, String msg) { + Map m = new LinkedHashMap<>(); + m.put("code", code); + m.put("msg", msg); + return m; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/application.yml b/inspect-main/inspect-main-agg/src/main/resources/application.yml new file mode 100644 index 0000000..ebd3ac1 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/application.yml @@ -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 + diff --git a/inspect-main/inspect-main-agg/src/main/resources/db/schema.sql b/inspect-main/inspect-main-agg/src/main/resources/db/schema.sql new file mode 100644 index 0000000..b3c87c7 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/db/schema.sql @@ -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'; diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggAnalysisDefectMonthMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggAnalysisDefectMonthMapper.xml new file mode 100644 index 0000000..eaf9406 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggAnalysisDefectMonthMapper.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + 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) + + + DELETE FROM agg_analysis_defect_month + WHERE station_code = #{stationCode} + AND stat_month >= #{beginMonth} + AND stat_month < #{endMonth} + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataAreaMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataAreaMapper.xml new file mode 100644 index 0000000..34ce574 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataAreaMapper.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + UPDATE agg_basedata_area + SET is_deleted = 1, synced_at = NOW() + WHERE station_code = #{stationCode} AND is_deleted = 0 + + AND area_id NOT IN + #{id} + + + + + + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataDeviceMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataDeviceMapper.xml new file mode 100644 index 0000000..4f3c9ac --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataDeviceMapper.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + UPDATE agg_basedata_device + SET is_deleted = 1, synced_at = NOW() + WHERE station_code = #{stationCode} AND is_deleted = 0 + + AND device_id NOT IN + #{id} + + + + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataDevicetypeMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataDevicetypeMapper.xml new file mode 100644 index 0000000..e0e0679 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataDevicetypeMapper.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + UPDATE agg_basedata_devicetype + SET is_deleted = 1, synced_at = NOW() + WHERE station_code = #{stationCode} AND is_deleted = 0 + + AND device_type_id NOT IN + #{id} + + + + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataEqpbookChannelMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataEqpbookChannelMapper.xml new file mode 100644 index 0000000..6636f66 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataEqpbookChannelMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + UPDATE agg_basedata_eqpbook_channel + SET is_deleted = 1, synced_at = NOW() + WHERE station_code = #{stationCode} AND is_deleted = 0 + + AND channel_id NOT IN + #{id} + + + + + + + + diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataEqpbookMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataEqpbookMapper.xml new file mode 100644 index 0000000..9c572b9 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataEqpbookMapper.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + UPDATE agg_basedata_eqpbook + SET is_deleted = 1, synced_at = NOW() + WHERE station_code = #{stationCode} AND is_deleted = 0 + + AND eqpbook_id NOT IN + #{id} + + + + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataPatrolpointMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataPatrolpointMapper.xml new file mode 100644 index 0000000..4f58c3d --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggBasedataPatrolpointMapper.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO agg_basedata_patrolpoint (station_code, patrol_point_id, patrol_point_code, patrol_point_name, main_device_id, device_id, area_id, area_name, patrol_content, purpose, patrol_type_id, point_impt_level, alarm_threshold, alg_subtype_ids, update_time, synced_at, is_deleted) + VALUES (#{stationCode}, #{patrolPointId}, #{patrolPointCode}, #{patrolPointName}, #{mainDeviceId}, #{deviceId}, #{areaId}, #{areaName}, #{patrolContent}, #{purpose}, #{patrolTypeId}, #{pointImptLevel}, #{alarmThreshold}, #{algSubtypeIds}, #{updateTime}, #{syncedAt}, #{isDeleted}) + ON DUPLICATE KEY UPDATE + patrol_point_code = VALUES(patrol_point_code), + patrol_point_name = VALUES(patrol_point_name), + main_device_id = VALUES(main_device_id), + device_id = VALUES(device_id), + area_id = VALUES(area_id), + area_name = VALUES(area_name), + patrol_content = VALUES(patrol_content), + purpose = VALUES(purpose), + patrol_type_id = VALUES(patrol_type_id), + point_impt_level = VALUES(point_impt_level), + alarm_threshold = VALUES(alarm_threshold), + alg_subtype_ids = VALUES(alg_subtype_ids), + update_time = VALUES(update_time), + synced_at = VALUES(synced_at), + is_deleted = VALUES(is_deleted), + is_deleted = 0 + + + + + UPDATE agg_basedata_patrolpoint + SET is_deleted = 1, synced_at = NOW() + WHERE station_code = #{stationCode} AND is_deleted = 0 + + AND patrol_point_id NOT IN + #{id} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggDefectRecordMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggDefectRecordMapper.xml new file mode 100644 index 0000000..fa7108e --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggDefectRecordMapper.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + d.id, d.station_code, d.device_name, d.channel_name, d.channel_code, d.device_source, + d.point_name, d.point_id, d.warn_status, d.alg_name, d.res_value, d.img, d.filter_desc, + d.bright_desc, d.img_analyse, d.bright_img_analyse, d.defect_desc, d.line_id, d.patrol_time, + d.point_status, d.img_type, d.data_type, d.filter_value, d.request_id, d.res_status, + d.bright_res_status, d.area_name, d.area_id, d.alg_type, d.object_id, + d.image_normal_url_path, d.file_path, d.suggestion, d.original_value, d.task_patrol_id, + d.task_name, d.defect_level, d.synced_at, d.is_deleted + + + + INSERT INTO agg_defect_record ( + station_code, device_name, channel_name, channel_code, device_source, + point_name, point_id, warn_status, alg_name, res_value, img, filter_desc, + bright_desc, img_analyse, bright_img_analyse, defect_desc, line_id, patrol_time, + point_status, img_type, data_type, filter_value, request_id, res_status, + bright_res_status, area_name, area_id, alg_type, object_id, image_normal_url_path, + file_path, suggestion, original_value, task_patrol_id, task_name, defect_level, synced_at, is_deleted + ) VALUES ( + #{stationCode}, #{deviceName}, #{channelName}, #{channelCode}, #{deviceSource}, + #{pointName}, #{pointId}, #{warnStatus}, #{algName}, #{resValue}, #{img}, #{filterDesc}, + #{brightDesc}, #{imgAnalyse}, #{brightImgAnalyse}, #{desc}, #{lineId}, #{patrolTime}, + #{pointStatus}, #{imgType}, #{dataType}, #{filter}, #{requestId}, #{resStatus}, + #{brightResStatus}, #{areaName}, #{areaId}, #{algType}, #{objectId}, #{imageNormalUrlPath}, + #{filePath}, #{suggestion}, #{originalValue}, #{taskPatrolId}, #{taskName}, #{defectLevel}, #{syncedAt}, 0 + ) + ON DUPLICATE KEY UPDATE + device_name = VALUES(device_name), + channel_name = VALUES(channel_name), + channel_code = VALUES(channel_code), + device_source = VALUES(device_source), + point_name = VALUES(point_name), + point_id = VALUES(point_id), + warn_status = VALUES(warn_status), + alg_name = VALUES(alg_name), + res_value = VALUES(res_value), + img = VALUES(img), + filter_desc = VALUES(filter_desc), + bright_desc = VALUES(bright_desc), + img_analyse = VALUES(img_analyse), + bright_img_analyse = VALUES(bright_img_analyse), + defect_desc = VALUES(defect_desc), + line_id = VALUES(line_id), + patrol_time = VALUES(patrol_time), + point_status = VALUES(point_status), + img_type = VALUES(img_type), + data_type = VALUES(data_type), + filter_value = VALUES(filter_value), + request_id = VALUES(request_id), + res_status = VALUES(res_status), + bright_res_status = VALUES(bright_res_status), + area_name = VALUES(area_name), + area_id = VALUES(area_id), + image_normal_url_path = VALUES(image_normal_url_path), + file_path = VALUES(file_path), + suggestion = VALUES(suggestion), + original_value = VALUES(original_value), + task_name = VALUES(task_name), + defect_level = VALUES(defect_level), + synced_at = VALUES(synced_at), + is_deleted = 0 + + + + + + UPDATE agg_defect_record + SET is_deleted = 1, synced_at = NOW() + WHERE station_code = #{stationCode} AND is_deleted = 0 + AND id IN + #{id} + + + + + + diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggInspectionReportMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggInspectionReportMapper.xml new file mode 100644 index 0000000..735aee4 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggInspectionReportMapper.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO agg_inspection_report + (station_code, line_id, station_name, volt_level, station_type, inspection_date, + inspection_task_name, env_info, inspection_start_time, inspection_end_time, + patrol_statistics, check_person, check_time, description, file_path, + task_id, task_result_id, task_patrolled_id, location, filter, synced_at, is_deleted) + VALUES + (#{stationCode}, #{lineId}, #{stationName}, #{voltLevel}, #{stationType}, #{inspectionDate}, + #{inspectionTaskName}, #{envInfo}, #{inspectionStartTime}, #{inspectionEndTime}, + #{patrolStatistics}, #{checkPerson}, #{checkTime}, #{description}, #{filePath}, + #{taskId}, #{taskResultId}, #{taskPatrolledId}, #{location}, #{filter}, #{syncedAt}, #{isDeleted}) + ON DUPLICATE KEY UPDATE + station_name = VALUES(station_name), + volt_level = VALUES(volt_level), + station_type = VALUES(station_type), + inspection_date = VALUES(inspection_date), + inspection_task_name = VALUES(inspection_task_name), + env_info = VALUES(env_info), + inspection_start_time = VALUES(inspection_start_time), + inspection_end_time = VALUES(inspection_end_time), + patrol_statistics = VALUES(patrol_statistics), + check_person = VALUES(check_person), + check_time = VALUES(check_time), + description = VALUES(description), + file_path = VALUES(file_path), + task_id = VALUES(task_id), + task_result_id = VALUES(task_result_id), + task_patrolled_id = VALUES(task_patrolled_id), + location = VALUES(location), + filter = VALUES(filter), + synced_at = VALUES(synced_at), + is_deleted = 0 + + + + UPDATE agg_inspection_report + SET is_deleted = 1, synced_at = NOW() + WHERE station_code = #{stationCode} AND is_deleted = 0 + + AND line_id NOT IN + #{lineId} + + + + + WHERE r.is_deleted = 0 + AND r.station_code = #{stationCode} + + AND r.inspection_date >= STR_TO_DATE(#{cycleStartTime}, '%Y-%m-%d') + + + AND r.inspection_date < DATE_ADD(STR_TO_DATE(#{cycleEndTime}, '%Y-%m-%d'), INTERVAL 1 DAY) + + + AND r.inspection_task_name LIKE CONCAT('%', #{inspectionTaskName}, '%') + + + + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggPatrolTaskMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggPatrolTaskMapper.xml new file mode 100644 index 0000000..2ccf308 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggPatrolTaskMapper.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO agg_patrol_task (station_code, task_id, task_code, task_name, type, priority, device_level, device_list, fixed_start_time, is_enable, file_status, execution_status, task_cycle, cycle_month, cycle_week, cycle_execute_time, cycle_start_time, cycle_end_time, interval_number, interval_type, interval_execute_time, interval_start_time, interval_end_time, invalid_start_time, invalid_end_time, dev_type, area_id, area_name, point_type, total_points, execution_mode, drafter, specified_alg, create_time, update_time, synced_at, is_deleted) + VALUES (#{stationCode}, #{taskId}, #{taskCode}, #{taskName}, #{type}, #{priority}, #{deviceLevel}, #{deviceList}, #{fixedStartTime}, #{isEnable}, #{fileStatus}, #{executionStatus}, #{taskCycle}, #{cycleMonth}, #{cycleWeek}, #{cycleExecuteTime}, #{cycleStartTime}, #{cycleEndTime}, #{intervalNumber}, #{intervalType}, #{intervalExecuteTime}, #{intervalStartTime}, #{intervalEndTime}, #{invalidStartTime}, #{invalidEndTime}, #{devType}, #{areaId}, #{areaName}, #{pointType}, #{totalPoints}, #{executionMode}, #{drafter}, #{specifiedAlg}, #{createTime}, #{updateTime}, #{syncedAt}, #{isDeleted}) + ON DUPLICATE KEY UPDATE + task_code = VALUES(task_code), + task_name = VALUES(task_name), + type = VALUES(type), + priority = VALUES(priority), + device_level = VALUES(device_level), + device_list = VALUES(device_list), + fixed_start_time = VALUES(fixed_start_time), + is_enable = VALUES(is_enable), + file_status = VALUES(file_status), + execution_status = VALUES(execution_status), + task_cycle = VALUES(task_cycle), + cycle_month = VALUES(cycle_month), + cycle_week = VALUES(cycle_week), + cycle_execute_time = VALUES(cycle_execute_time), + cycle_start_time = VALUES(cycle_start_time), + cycle_end_time = VALUES(cycle_end_time), + interval_number = VALUES(interval_number), + interval_type = VALUES(interval_type), + interval_execute_time = VALUES(interval_execute_time), + interval_start_time = VALUES(interval_start_time), + interval_end_time = VALUES(interval_end_time), + invalid_start_time = VALUES(invalid_start_time), + invalid_end_time = VALUES(invalid_end_time), + dev_type = VALUES(dev_type), + area_id = VALUES(area_id), + area_name = VALUES(area_name), + point_type = VALUES(point_type), + total_points = VALUES(total_points), + execution_mode = VALUES(execution_mode), + drafter = VALUES(drafter), + specified_alg = VALUES(specified_alg), + create_time = VALUES(create_time), + update_time = VALUES(update_time), + synced_at = VALUES(synced_at), + is_deleted = VALUES(is_deleted), + is_deleted = 0 + + + + + UPDATE agg_patrol_task + SET is_deleted = 1, synced_at = NOW() + WHERE station_code = #{stationCode} AND is_deleted = 0 + + AND task_id NOT IN + #{id} + + + + + + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggPatrolTaskStatusMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggPatrolTaskStatusMapper.xml new file mode 100644 index 0000000..c3d3cef --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggPatrolTaskStatusMapper.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO agg_patrol_task_status (station_code, line_id, task_patrolled_id, task_name, task_code, task_state, plan_start_time, start_time, end_time, task_progress, task_estimated_time, patrol_type, pos_type, code, pos, exec_type, task_type, main_id, qx_num, file_status, summary, description, create_time, synced_at, is_deleted) + VALUES (#{stationCode}, #{lineId}, #{taskPatrolledId}, #{taskName}, #{taskCode}, #{taskState}, #{planStartTime}, #{startTime}, #{endTime}, #{taskProgress}, #{taskEstimatedTime}, #{patrolType}, #{posType}, #{code}, #{pos}, #{execType}, #{taskType}, #{mainId}, #{qxNum}, #{fileStatus}, #{summary}, #{description}, #{createTime}, #{syncedAt}, #{isDeleted}) + ON DUPLICATE KEY UPDATE + task_patrolled_id = VALUES(task_patrolled_id), + task_name = VALUES(task_name), + task_code = VALUES(task_code), + task_state = VALUES(task_state), + plan_start_time = VALUES(plan_start_time), + start_time = VALUES(start_time), + end_time = VALUES(end_time), + task_progress = VALUES(task_progress), + task_estimated_time = VALUES(task_estimated_time), + patrol_type = VALUES(patrol_type), + pos_type = VALUES(pos_type), + code = VALUES(code), + pos = VALUES(pos), + exec_type = VALUES(exec_type), + task_type = VALUES(task_type), + main_id = VALUES(main_id), + qx_num = VALUES(qx_num), + file_status = VALUES(file_status), + summary = VALUES(summary), + description = VALUES(description), + create_time = VALUES(create_time), + synced_at = VALUES(synced_at), + is_deleted = VALUES(is_deleted), + is_deleted = 0 + + + + + UPDATE agg_patrol_task_status + SET is_deleted = 1, synced_at = NOW() + WHERE station_code = #{stationCode} AND is_deleted = 0 + + AND line_id NOT IN + #{id} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggResultAnalysisMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggResultAnalysisMapper.xml new file mode 100644 index 0000000..1dbe62d --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggResultAnalysisMapper.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO agg_result_analysis + (station_code, line_id, result_type, business_id, result_content, res_status, point_status, + suggestion, object_id, description, request_id, task_patrol_id, alg_type, res_value, + res_code, res_conf, res_desc, result, res_img_url, image_normal_url_path, res_pos, + filter, original_value, defect_level, create_time, synced_at, is_deleted) + VALUES + (#{stationCode}, #{lineId}, #{resultType}, #{businessId}, #{resultContent}, #{resStatus}, #{pointStatus}, + #{suggestion}, #{objectId}, #{description}, #{requestId}, #{taskPatrolId}, #{algType}, #{resValue}, + #{resCode}, #{resConf}, #{resDesc}, #{result}, #{resImgUrl}, #{imageNormalUrlPath}, #{resPos}, + #{filter}, #{originalValue}, #{defectLevel}, #{createTime}, #{syncedAt}, #{isDeleted}) + ON DUPLICATE KEY UPDATE + result_type = VALUES(result_type), + business_id = VALUES(business_id), + result_content = VALUES(result_content), + res_status = VALUES(res_status), + point_status = VALUES(point_status), + suggestion = VALUES(suggestion), + object_id = VALUES(object_id), + description = VALUES(description), + request_id = VALUES(request_id), + task_patrol_id = VALUES(task_patrol_id), + alg_type = VALUES(alg_type), + res_value = VALUES(res_value), + res_code = VALUES(res_code), + res_conf = VALUES(res_conf), + res_desc = VALUES(res_desc), + result = VALUES(result), + res_img_url = VALUES(res_img_url), + image_normal_url_path = VALUES(image_normal_url_path), + res_pos = VALUES(res_pos), + filter = VALUES(filter), + original_value = VALUES(original_value), + defect_level = VALUES(defect_level), + create_time = VALUES(create_time), + synced_at = VALUES(synced_at), + is_deleted = 0 + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggStationMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggStationMapper.xml new file mode 100644 index 0000000..ff1c60f --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggStationMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/AggSyncLogMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggSyncLogMapper.xml new file mode 100644 index 0000000..923f991 --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/AggSyncLogMapper.xml @@ -0,0 +1,9 @@ + + + + + + INSERT INTO agg_sync_log (station_code, entity, sync_type, status, row_count, deleted_count, message, started_at, finished_at) + VALUES (#{stationCode}, #{entity}, #{syncType}, #{status}, #{rowCount}, #{deletedCount}, #{message}, #{startedAt}, #{finishedAt}) + + \ No newline at end of file diff --git a/inspect-main/inspect-main-agg/src/main/resources/mapper/BasedataAlgSubtypeMapper.xml b/inspect-main/inspect-main-agg/src/main/resources/mapper/BasedataAlgSubtypeMapper.xml new file mode 100644 index 0000000..cf22bca --- /dev/null +++ b/inspect-main/inspect-main-agg/src/main/resources/mapper/BasedataAlgSubtypeMapper.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/constant/YanyuanConstants.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/constant/YanyuanConstants.java index 3a1d55c..984e4f5 100644 --- a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/constant/YanyuanConstants.java +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/constant/YanyuanConstants.java @@ -4,6 +4,8 @@ public class YanyuanConstants { private YanyuanConstants() {} + public static final String WS_ENABLED = "ws-enabled"; + public static final String EXCHANGE = "alarm.exchange"; public static final String QUEUE = "alarm.queue"; public static final String ROUTING_KEY = "alarm.routing-key"; diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/AlarmConsumer.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/AlarmConsumer.java index a4376af..54f7023 100644 --- a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/AlarmConsumer.java +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/AlarmConsumer.java @@ -10,12 +10,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import java.util.Map; @Slf4j @Component +@ConditionalOnProperty(name = YanyuanConstants.WS_ENABLED, havingValue = "true") public class AlarmConsumer { /** 最多重试次数(总尝试次数 = MAX_RETRY + 1) */ diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportUploadService.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportUploadService.java index f02fa4d..44b05d4 100644 --- a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportUploadService.java +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ReportUploadService.java @@ -33,7 +33,7 @@ public class ReportUploadService { private static final String TOKEN_HEADER = "joinbright-token"; /** 是否启用报告上传推送 */ - @Value("${inspect.report-push.enabled:true}") + @Value("${inspect.report-push.enabled:false}") private boolean enabled; /** 巡检报告上传接口完整地址 */ diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/websocket/AlarmWsConnection.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/websocket/AlarmWsConnection.java index 15ea0f1..68c631e 100644 --- a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/websocket/AlarmWsConnection.java +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/websocket/AlarmWsConnection.java @@ -1,10 +1,12 @@ package com.inspect.api.websocket; +import com.inspect.api.constant.YanyuanConstants; import com.inspect.api.domain.ApiResult; import com.inspect.api.domain.AuthTokenData; import com.inspect.api.props.AppProperties; import com.inspect.api.service.ExtAuthService; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; @@ -18,6 +20,7 @@ import java.util.concurrent.atomic.AtomicReference; @Slf4j @Component +@ConditionalOnProperty(name = YanyuanConstants.WS_ENABLED, havingValue = "true") public class AlarmWsConnection { private final ExtAuthService extAuthService; private final AppProperties appProperties; diff --git a/inspect-main/inspect-main-start/pom.xml b/inspect-main/inspect-main-start/pom.xml index c4a084d..5f47224 100644 --- a/inspect-main/inspect-main-start/pom.xml +++ b/inspect-main/inspect-main-start/pom.xml @@ -70,6 +70,10 @@ com.inspect inspect-main-api + + com.inspect + inspect-main-agg + diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/controller/ResultAnalysisController.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/controller/ResultAnalysisController.java index 5b76671..f2a05b1 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/controller/ResultAnalysisController.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/controller/ResultAnalysisController.java @@ -13,6 +13,7 @@ import com.inspect.base.core.domain.Response; import com.inspect.base.core.enums.PointStatusEnum; import com.inspect.base.core.utils.StringUtils; import com.inspect.base.core.utils.poi.ExcelUtil; +import com.inspect.base.core.enums.DefectLevel; import com.inspect.base.core.web.controller.BaseController; import com.inspect.base.core.web.domain.AjaxResult; import com.inspect.base.core.web.page.PageDomain; @@ -33,6 +34,9 @@ import com.inspect.task.service.IPatrolTaskService; import com.inspect.taskinfo.domain.PatrolTaskInfo; import com.inspect.taskinfo.service.IPatrolTaskInfoService; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; import javax.annotation.Resource; @@ -44,6 +48,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -300,6 +305,53 @@ public class ResultAnalysisController extends BaseController { return this.getDataTable(patrolResultList); } + /** + * 汇聚专用:按时间范围分页查询 result_analysis 原始结果。 + */ + @GetMapping({"/agg/list"}) + public TableDataInfo aggList(@RequestParam(required = false) String beginTime, + @RequestParam(required = false) String endTime, + @RequestParam(required = false) String taskPatrolId) { + this.startPage(); + ResultAnalysis query = new ResultAnalysis(); + query.setBeginTime(beginTime); + query.setEndTime(endTime); + query.setTaskPatrolId(taskPatrolId); + List list = this.resultAnalysisService.selectResultAnalysisList(query); + for (ResultAnalysis item : list) { + item.setDefectLevel(resolveDefectLevel(item.getResultType())); + } + return this.getDataTable(list); + } + + /** 汇聚专用:按月份聚合 defect 统计结果。 */ + @GetMapping({"/agg/defect/month"}) + public AjaxResult aggDefectMonth(@RequestParam String beginMonth, @RequestParam String endMonth) { + if (StringUtils.isEmpty(beginMonth) || StringUtils.isEmpty(endMonth)) { + return AjaxResult.error("beginMonth / endMonth 不能为空"); + } + try { + DateTimeFormatter month = DateTimeFormatter.ofPattern("yyyy-MM"); + DateTimeFormatter datetime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + LocalDate begin = YearMonth.parse(beginMonth, month).atDay(1); + LocalDate end = YearMonth.parse(endMonth, month).plusMonths(1).atDay(1); + List> list = resultAnalysisService.selectAggDefectMonthStat( + begin.atStartOfDay().format(datetime), end.atStartOfDay().format(datetime)); + return AjaxResult.success(list); + } catch (Exception e) { + return AjaxResult.error("月度缺陷统计失败: " + e.getMessage()); + } + } + private static Integer resolveDefectLevel(String resultType) { + if ("0".equals(resultType)) { + return DefectLevel.NORMAL.getCode(); + } + if ("2".equals(resultType)) { + return DefectLevel.MINOR.getCode(); + } + return DefectLevel.NONE.getCode(); + } + @Log( title = "分析结果", businessType = BizType.EXPORT diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/AlgAlarmResult.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/AlgAlarmResult.java new file mode 100644 index 0000000..e6d7a78 --- /dev/null +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/AlgAlarmResult.java @@ -0,0 +1,67 @@ +package com.inspect.analysis.domain; + +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; + +/** + * 算法值越限分析结果。 + */ +@Getter +@Setter +public class AlgAlarmResult { + + public enum Status { + NORMAL, + OVER_MAX, + BELOW_MIN, + PARSE_ERROR + } + + private boolean alarm; + private Status status; + private String value; + private Double max; + private Double min; + /** 超过最大值多少(正数):value - max */ + private Double overValue; + /** 低于最小值多少(正数):min - value */ + private Double belowValue; + + public AlgAlarmResult() { + this.status = Status.NORMAL; + } + + /** 中文描述后缀,便于拼接告警文案。 */ + public String messageSuffix() { + switch (status) { + case OVER_MAX: + return "(超过上限 " + plain(max) + ",超限值 " + plain(overValue) + ")"; + case BELOW_MIN: + return "(低于下限 " + plain(min) + ",低限值 " + plain(belowValue) + ")"; + default: + return ""; + } + } + + private static String plain(Double value) { + if (value == null) { + return ""; + } + return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString(); + } + + @Override + public String toString() { + return "AlgAlarmResult{" + + "alarm=" + alarm + + ", status=" + status + + ", value='" + value + '\'' + + ", max=" + max + + ", min=" + min + + ", overValue=" + overValue + + ", belowValue=" + belowValue + + '}'; + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/AlgValue.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/AlgValue.java index a0023a0..913f5a6 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/AlgValue.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/AlgValue.java @@ -5,6 +5,8 @@ import com.inspect.base.core.utils.StringUtils; import lombok.Getter; import lombok.Setter; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Objects; @Setter @@ -14,26 +16,47 @@ public class AlgValue { private String min; public boolean isAlarm(String value) { + return analyze(value).isAlarm(); + } + + /** + * 分析当前值:正常 / 超过上限 / 低于下限 / 解析失败,并返回偏差量。 + * 偏差量使用 BigDecimal 计算并保留 2 位小数,避免浮点误差。 + */ + public AlgAlarmResult analyze(String value) { + AlgAlarmResult result = new AlgAlarmResult(); + result.setValue(value); try { - double valueD = Double.parseDouble(value); - double tmp; - if(!StringUtils.isEmpty(this.max)) { - tmp = Double.parseDouble(this.max); - if(valueD > tmp) { - return true; + BigDecimal valueD = new BigDecimal(value.trim()); + if (!StringUtils.isEmpty(this.max)) { + BigDecimal maxD = new BigDecimal(this.max.trim()); + result.setMax(maxD.doubleValue()); + if (valueD.compareTo(maxD) > 0) { + result.setAlarm(true); + result.setStatus(AlgAlarmResult.Status.OVER_MAX); + result.setOverValue(valueD.subtract(maxD).setScale(2, RoundingMode.HALF_UP).doubleValue()); + return result; } } - if(!StringUtils.isEmpty(this.min)) { - tmp = Double.parseDouble(this.min); - if(valueD < tmp) { - return true; + if (!StringUtils.isEmpty(this.min)) { + BigDecimal minD = new BigDecimal(this.min.trim()); + result.setMin(minD.doubleValue()); + if (valueD.compareTo(minD) < 0) { + result.setAlarm(true); + result.setStatus(AlgAlarmResult.Status.BELOW_MIN); + result.setBelowValue(minD.subtract(valueD).setScale(2, RoundingMode.HALF_UP).doubleValue()); + return result; } } - return false; + result.setAlarm(false); + result.setStatus(AlgAlarmResult.Status.NORMAL); + return result; } catch (NumberFormatException e) { - return true; + result.setAlarm(true); + result.setStatus(AlgAlarmResult.Status.PARSE_ERROR); + return result; } } @@ -83,4 +106,4 @@ public class AlgValue { ", min='" + min + '\'' + '}'; } -} +} \ No newline at end of file diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/ResultAnalysis.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/ResultAnalysis.java index 903ae14..484d0e4 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/ResultAnalysis.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/domain/ResultAnalysis.java @@ -46,6 +46,8 @@ public class ResultAnalysis extends BaseEntity { private String algType; private String filter; private String taskPatrolId; + /** 当前由 resultType/warnStatus 推导,后续可改为表字段 */ + private Integer defectLevel; private String resCode; private String resImgUrl; private String resConf; diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/mapper/ResultAnalysisMapper.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/mapper/ResultAnalysisMapper.java index 6506198..1d39eb5 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/mapper/ResultAnalysisMapper.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/mapper/ResultAnalysisMapper.java @@ -82,4 +82,7 @@ public interface ResultAnalysisMapper { void updateResultAnalysisByCondition(ResultAnalysis analysis); List countResultAnalysisSummary(@Param("list") List taskPatrolIds); -} + + List> selectAggDefectMonthStat(@Param("beginTime") String beginTime, + @Param("endTime") String endTime); +} \ No newline at end of file diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/IResultAnalysisService.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/IResultAnalysisService.java index 0ecc153..7c636c2 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/IResultAnalysisService.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/IResultAnalysisService.java @@ -62,4 +62,6 @@ public interface IResultAnalysisService { void updateResultAnalysisByCondition(ResultAnalysis analysis); List countResultAnalysisSummary(List taskPatrolIds); + + List> selectAggDefectMonthStat(String beginTime, String endTime); } diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/impl/ResultAnalysisServiceImpl.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/impl/ResultAnalysisServiceImpl.java index 3cc4cee..8f84dcb 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/impl/ResultAnalysisServiceImpl.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/service/impl/ResultAnalysisServiceImpl.java @@ -201,4 +201,8 @@ public class ResultAnalysisServiceImpl implements IResultAnalysisService { return this.resultAnalysisMapper.selectResultAnalysisListDefectInfo(defectFilter, defectIsToday, type); } -} + @Override + public List> selectAggDefectMonthStat(String beginTime, String endTime) { + return this.resultAnalysisMapper.selectAggDefectMonthStat(beginTime, endTime); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/utils/ResultAnalysisUtils.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/utils/ResultAnalysisUtils.java index 2274f79..5de4a5f 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/utils/ResultAnalysisUtils.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/utils/ResultAnalysisUtils.java @@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.inspect.analysis.constant.AnalyseConstants; +import com.inspect.analysis.domain.AlgAlarmResult; import com.inspect.analysis.domain.AlgValue; import com.inspect.analysis.domain.AnalyseResPoint; import com.inspect.analysis.domain.ResultAnalysis; @@ -175,13 +176,14 @@ public class ResultAnalysisUtils { } else if (algType.equals("meter") || algType.equals("gis_meter")) { log.info("------------------parseAlg : meter|gis_meter value: {}, originalValue: {}", value, correctionValue); if ("open".equalsIgnoreCase(value) - || "closed".equalsIgnoreCase(value) || "Off".equalsIgnoreCase(value) || "On".equalsIgnoreCase(value) || "正常".equalsIgnoreCase(value)) { + || "closed".equalsIgnoreCase(value) || "Off".equalsIgnoreCase(value) || "On".equalsIgnoreCase(value) || "正常".equals(value)) { resultAnalysis.setResStatus("0"); resultAnalysis.setPointStatus("1"); resultAnalysis.setResultType("1"); defaultDesc = messageUtils.get("正常"); } else { - if (!algValue.isAlarm(value)) { + AlgAlarmResult algAlarmResult = algValue.analyze(value); + if (!algAlarmResult.isAlarm()) { log.info("PARSE_ALG_METER_NO_ALARM value: {}, defaultDesc: {}", value, defaultDesc); if (StringUtils.isNotEmpty(defaultDesc) && defaultDesc.contains("失败")) { resultAnalysis.setResStatus("0"); @@ -195,7 +197,7 @@ public class ResultAnalysisUtils { defaultDesc = messageUtils.get("正常"); } } else { - log.info("PARSE_ALG_METER_ALARM value: {}", value); + log.info("PARSE_ALG_METER_ALARM value: {}, result: {}", value, algAlarmResult); String[] valueArr = value.split(StringUtils.COMMA); if (valueArr.length > 1) { log.info("PARSE_ALG_METER_ALARM_VALUE_COMPOSITE value: {}", value); @@ -219,7 +221,7 @@ public class ResultAnalysisUtils { resultAnalysis.setResStatus("0"); resultAnalysis.setPointStatus(PointStatusEnum.DEFECT.getCode()); resultAnalysis.setResultType("0"); - defaultDesc = messageUtils.get("表计超限"); + defaultDesc = messageUtils.get("表计超限") + algAlarmResult.messageSuffix(); } catch (Exception e) { log.info("PARSE_ALG_METER_ALARM_VALUE_SINGLE_NO_DIGIT value: {}", value); resultAnalysis.setResStatus("0"); @@ -251,9 +253,10 @@ public class ResultAnalysisUtils { values[i] = "--"; descs[i] = "读数失败"; } else { - if (algValue.isAlarm(item)) { + AlgAlarmResult itemAlarm = algValue.analyze(item); + if (itemAlarm.isAlarm()) { hasDefect = true; - descs[i] = "表计超限"; + descs[i] = "表计超限" + itemAlarm.messageSuffix(); } else { descs[i] = "正常"; } @@ -295,7 +298,8 @@ public class ResultAnalysisUtils { } else if ( AlgConstants.ALG_INFRARED_LIST.contains(algType) || algType.equals("xb")) { - if (!algValue.isAlarm(value)) { + AlgAlarmResult infraredAlarm = algValue.analyze(value); + if (!infraredAlarm.isAlarm()) { log.info("infrared no alarm value: {}, algValue: {}", value, algValue); resultAnalysis.setResStatus("1"); resultAnalysis.setPointStatus("1"); @@ -303,13 +307,13 @@ public class ResultAnalysisUtils { //defaultDesc = device + messageUtils.get("正常"); defaultDesc = messageUtils.get("正常"); } else { - log.info("infrared alarm value: {}, algValue: {}", value, algValue); + log.info("infrared alarm value: {}, algValue: {}, result: {}", value, algValue, infraredAlarm); resultAnalysis.setResStatus("0"); //resultAnalysis.setPointStatus("2"); resultAnalysis.setPointStatus(PointStatusEnum.DEFECT.getCode()); resultAnalysis.setResultType("0"); //defaultDesc = device + messageUtils.get("告警"); - defaultDesc = messageUtils.get("温度异常告警"); + defaultDesc = messageUtils.get("温度异常告警") + infraredAlarm.messageSuffix(); } if (messageUtils.get("分析失败").equals(defaultDesc)) { defaultDesc = "--"; diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/insreporttask/mapper/InspectionReportTaskMapper.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/insreporttask/mapper/InspectionReportTaskMapper.java index 0fd85ce..2f628a4 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/insreporttask/mapper/InspectionReportTaskMapper.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/insreporttask/mapper/InspectionReportTaskMapper.java @@ -25,4 +25,12 @@ public interface InspectionReportTaskMapper { */ List selectInspectionReportsByTaskPatrolledId(@Param("taskPatrolledId") String taskPatrolledId, @Param("filter") String filter); + + /** + * 按报告主键查询该报告关联的所有任务执行标识。 + * + * @param reportLineId 报告主键 inspection_report.line_id + * @return 任务执行标识列表 inspection_report_task.task_patrolled_id + */ + List selectTaskPatrolledIdsByReportLineId(@Param("reportLineId") Long reportLineId); } diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/config/ReportPushProperties.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/config/ReportPushProperties.java index b5333bd..0ddd06f 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/config/ReportPushProperties.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/config/ReportPushProperties.java @@ -18,10 +18,10 @@ import org.springframework.stereotype.Component; public class ReportPushProperties { /** 是否启用推送 */ - private boolean enabled = true; + private boolean enabled = false; /** 是否启用自动归档 */ - private boolean archive = true; + private boolean archive = false; /** 自动归档审核人 */ private String checkPerson = "admin"; diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/service/ReportPushService.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/service/ReportPushService.java index ff28941..a97ce3b 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/service/ReportPushService.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/reportpush/service/ReportPushService.java @@ -101,8 +101,8 @@ public class ReportPushService { message.setFilePath(filePath); message.setReportInfo(reportInfo.toJSONString()); reportPushPublisher.publish(message); - log.info("[REPORT_PUSH] 报告推送消息已发布: reportId={}, filePath={}, username={}", - report.getLineId(), filePath, username); + log.info("[REPORT_PUSH] 报告推送消息已发布: reportId={}, filePath={}, username={}, message={}", + report.getLineId(), filePath, username, message); } /** diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/DonghuanReportExportController.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/DonghuanReportExportController.java index fb03cf6..3424499 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/DonghuanReportExportController.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/resultmain/controller/DonghuanReportExportController.java @@ -13,6 +13,7 @@ import com.inspect.insreport.domain.InspectionReport; import com.inspect.insreport.service.IInspectionReportService; import com.inspect.insreportdata.domain.InspectionReportData; import com.inspect.insreportdata.service.IInspectionReportDataService; +import com.inspect.insreporttask.mapper.InspectionReportTaskMapper; import com.inspect.message.MessageUtils; import com.inspect.partrolresult.domain.PatrolResult; import com.inspect.partrolresult.service.IPatrolResultService; @@ -279,6 +280,7 @@ public class DonghuanReportExportController extends BaseController { private final IInspectionReportDataService inspectionReportDataService; private final IInspectionReportImgService inspectionReportImgService; private final IInspectionReportService inspectionReportService; + private final InspectionReportTaskMapper inspectionReportTaskMapper; private final SftpClient sftpClient; private final ReportPushService reportPushService; private final ReportPushProperties reportPushProperties; @@ -294,6 +296,7 @@ public class DonghuanReportExportController extends BaseController { IInspectionReportDataService inspectionReportDataService, IInspectionReportImgService inspectionReportImgService, IInspectionReportService inspectionReportService, + InspectionReportTaskMapper inspectionReportTaskMapper, SftpClient sftpClient, ReportPushService reportPushService, ReportPushProperties reportPushProperties, @@ -303,6 +306,7 @@ public class DonghuanReportExportController extends BaseController { this.inspectionReportDataService = inspectionReportDataService; this.inspectionReportImgService = inspectionReportImgService; this.inspectionReportService = inspectionReportService; + this.inspectionReportTaskMapper = inspectionReportTaskMapper; this.sftpClient = sftpClient; this.reportPushService = reportPushService; this.reportPushProperties = reportPushProperties; @@ -589,6 +593,11 @@ public class DonghuanReportExportController extends BaseController { try { inspectionReport.setFilePath(basePath + reportName + ".docx"); inspectionReport.setLineIds(reportIds); + // 报告的 task_patrolled_id:按 report_line_id 取 inspection_report_task 下所有任务执行标识 + List taskPatrolledIds = inspectionReportTaskMapper + .selectTaskPatrolledIdsByReportLineId(lineId); + inspectionReport.setTaskPatrolledId( + taskPatrolledIds == null ? null : StringUtils.join(taskPatrolledIds, ",")); inspectionReportService.updateInspectionReportFilePath(inspectionReport); // 报告生成并上传完成后,发布消息到 RabbitMQ,由消费者异步 HTTP 上传外部平台(自动归档与手动归档均推送) reportPushService.pushReport(inspectionReport, username); diff --git a/inspect-main/inspect-main-task/src/main/java/com/inspect/task/domain/PatrolData.java b/inspect-main/inspect-main-task/src/main/java/com/inspect/task/domain/PatrolData.java index e753afa..2daa47d 100644 --- a/inspect-main/inspect-main-task/src/main/java/com/inspect/task/domain/PatrolData.java +++ b/inspect-main/inspect-main-task/src/main/java/com/inspect/task/domain/PatrolData.java @@ -105,9 +105,9 @@ public class PatrolData implements Comparable { public void setWarnStatus(String warnStatus) { this.warnStatus = warnStatus; if ("0".equals(warnStatus)) { - this.defectLevel = DefectLevel.SERIOUS.getCode(); - } else if ("2".equals(warnStatus)) { this.defectLevel = DefectLevel.NORMAL.getCode(); + } else if ("2".equals(warnStatus)) { + this.defectLevel = DefectLevel.MINOR.getCode(); } else { this.defectLevel = DefectLevel.NONE.getCode(); } diff --git a/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportTaskMapper.xml b/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportTaskMapper.xml index 9e5df03..f5828fc 100644 --- a/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportTaskMapper.xml +++ b/inspect-main/inspect-main-task/src/main/resources/mapper/task/InspectionReportTaskMapper.xml @@ -61,4 +61,9 @@ and r.filter = #{filter} order by r.line_id desc + + diff --git a/inspect-main/inspect-main-task/src/main/resources/mapper/task/ResultAnalysisMapper.xml b/inspect-main/inspect-main-task/src/main/resources/mapper/task/ResultAnalysisMapper.xml index fdc6bf4..f288a97 100644 --- a/inspect-main/inspect-main-task/src/main/resources/mapper/task/ResultAnalysisMapper.xml +++ b/inspect-main/inspect-main-task/src/main/resources/mapper/task/ResultAnalysisMapper.xml @@ -91,6 +91,7 @@ res_desc, res_pos, result, + original_value, image_normal_url_path from result_analysis @@ -758,4 +759,65 @@ + diff --git a/inspect-main/pom.xml b/inspect-main/pom.xml index c5dbf44..088d59a 100644 --- a/inspect-main/pom.xml +++ b/inspect-main/pom.xml @@ -21,6 +21,7 @@ inspect-main-video inspect-main-task-exec inspect-main-api + inspect-main-agg diff --git a/pom.xml b/pom.xml index 1e4c753..4575d65 100644 --- a/pom.xml +++ b/pom.xml @@ -351,6 +351,11 @@ inspect-main-api 3.4.0 + + com.inspect + inspect-main-agg + 3.4.0 + diff --git a/智能巡视系统对外接口规范文档_v1.5.docx b/智能巡视系统对外接口规范文档_v1.5.docx new file mode 100644 index 0000000..f8d4bb1 Binary files /dev/null and b/智能巡视系统对外接口规范文档_v1.5.docx differ