| | |
| | | import java.net.InetAddress; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.regex.Pattern; |
| | | import java.time.Instant; |
| | | import java.util.Map; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.battery.business.domain.TCollector; |
| | | import com.battery.business.domain.TDeviceAdapter; |
| | | import com.battery.business.mapper.TCollectorMapper; |
| | | import com.battery.business.mapper.TDeviceAdapterMapper; |
| | | import com.battery.business.service.ITCollectorService; |
| | | import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore; |
| | | import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CollectorCandidate; |
| | | |
| | | /** |
| | | * 采集器 Service 实现。 |
| | | * |
| | | * <p>采集器按 ADR-0064 不建独立设备模型:它的测量是「同一来源的辅助测量」, |
| | | * 由所链接的来源实例提供。因此这里不再走发现候选(真实来源根本不产出采集器候选), |
| | | * 而是由操作员手工登记来源身份,在线状态直接取所链接来源实例的连接结果。 |
| | | * |
| | | * @author ruoyi |
| | | */ |
| | |
| | | private static final String ENABLED = "0"; |
| | | private static final String DISABLED = "1"; |
| | | private static final String OFFLINE = "0"; |
| | | private static final String ONLINE = "1"; |
| | | private static final String IDLE = "0"; |
| | | private static final String TESTING = "1"; |
| | | private static final Pattern IPV4_PATTERN = Pattern.compile( |
| | |
| | | private static final Pattern MAC_PATTERN = Pattern.compile("^[0-9A-F]{2}(:[0-9A-F]{2}){5}$"); |
| | | |
| | | private final TCollectorMapper collectorMapper; |
| | | private final DeviceDiscoveryCandidateStore candidateStore; |
| | | private final TDeviceAdapterMapper adapterMapper; |
| | | |
| | | public TCollectorServiceImpl(TCollectorMapper collectorMapper, DeviceDiscoveryCandidateStore candidateStore) |
| | | public TCollectorServiceImpl(TCollectorMapper collectorMapper, TDeviceAdapterMapper adapterMapper) |
| | | { |
| | | this.collectorMapper = collectorMapper; |
| | | this.candidateStore = candidateStore; |
| | | this.adapterMapper = adapterMapper; |
| | | } |
| | | |
| | | /** |
| | |
| | | { |
| | | throw new ServiceException("采集器不存在"); |
| | | } |
| | | return applyRealtimeState(collector, candidatesByCode()); |
| | | return collector; |
| | | } |
| | | |
| | | /** |
| | |
| | | collector.setEndCreateTime(trimToNull(collector.getEndCreateTime())); |
| | | String requestedOnlineStatus = collector.getOnlineStatus(); |
| | | validateOptionalStatus(requestedOnlineStatus, "在线状态"); |
| | | // 在线状态由 SQL 从所链接来源实例的连接结果派生,这里不再从存储列过滤。 |
| | | collector.setOnlineStatus(null); |
| | | validateOptionalStatus(collector.getTestStatus(), "测试状态"); |
| | | validateOptionalStatus(collector.getStatus(), "状态"); |
| | | Map<String, CollectorCandidate> candidates = candidatesByCode(); |
| | | return collectorMapper.selectCollectorList(collector).stream() |
| | | .map(item -> applyRealtimeState(item, candidates)) |
| | | .filter(item -> StringUtils.isEmpty(requestedOnlineStatus) |
| | | || requestedOnlineStatus.equals(item.getOnlineStatus())) |
| | | .toList(); |
| | | return collectorMapper.selectCollectorList(collector); |
| | | } |
| | | |
| | | /** |
| | | * 新增采集器并初始化运行状态。 |
| | | * |
| | | * <p>前置条件:设备编码与名称非空且设备编码未登记过;IP 与 MAC 可留空(辅助测量不是独立硬件, |
| | | * 系统不连它),填了则校验格式。在线状态取所链接来源实例的连接结果,未链接来源时保持离线。 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int insertCollector(TCollector collector) |
| | | { |
| | | if (collector == null) throw new ServiceException("采集器信息不能为空"); |
| | | collector.setCollectorCode(trimToNull(collector.getCollectorCode())); |
| | | CollectorCandidate candidate = candidateStore.findCollector(collector.getCollectorCode(), Instant.now()) |
| | | .orElseThrow(() -> new ServiceException("请先选择有效的发现候选设备")); |
| | | collector.setIpAddress(candidate.ip()); |
| | | collector.setMacAddress(candidate.mac()); |
| | | collector.setChannelCount(candidate.channelCount()); |
| | | validateCollector(collector, false); |
| | | requireLinkedAdapter(collector); |
| | | collector.setOnlineStatus(OFFLINE); |
| | | // 测试占用由 t_test_collector_channel + t_test 派生,采集器登记表不写入这些字段。 |
| | | collector.setTestStatus(IDLE); |
| | |
| | | } |
| | | collector.setDelFlag("0"); |
| | | collector.setCreateTime(new Date()); |
| | | int rows = collectorMapper.insertCollector(collector); |
| | | // 登记只确认来源身份,在线与离线事实都按候选原样同步。 |
| | | collectorMapper.updateRegisteredObservation(collector.getCollectorCode(), |
| | | candidate.online() ? "ONLINE" : "OFFLINE"); |
| | | return rows; |
| | | // 登记只确认来源身份:在线事实由所链接来源实例的连接结果表达,登记时不改写它。 |
| | | return collectorMapper.insertCollector(collector); |
| | | } |
| | | |
| | | /** |
| | |
| | | if (current == null) throw new ServiceException("采集器不存在"); |
| | | if (collector.getCollectorCode() != null && !collector.getCollectorCode().trim().equals(current.getCollectorCode())) |
| | | throw new ServiceException("设备编码不可修改"); |
| | | // 编辑只提交可改字段:未提交的 IP/MAC 不能被当成"清空"。 |
| | | fillMissingFromCurrent(collector, current); |
| | | validateCollector(collector, true); |
| | | requireLinkedAdapter(collector); |
| | | validateStatus(collector.getStatus(), "状态"); |
| | | if (DISABLED.equals(collector.getStatus())) |
| | | { |
| | |
| | | } |
| | | collector.setUpdateTime(new Date()); |
| | | return collectorMapper.updateCollectorStatus(collector); |
| | | } |
| | | |
| | | /** 查询全部未删除采集器的设备编码,供发现候选排除已登记设备。 */ |
| | | @Override |
| | | public Set<String> selectRegisteredCollectorCodes() |
| | | { |
| | | return Set.copyOf(collectorMapper.selectRegisteredCollectorCodes()); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | collector.setCollectorCode(trimToNull(collector.getCollectorCode())); |
| | | collector.setIpAddress(trimToNull(collector.getIpAddress())); |
| | | collector.setMacAddress(normalizeMacAddress(collector.getMacAddress())); |
| | | collector.setMacAddress(normalizeOptionalMacAddress(collector.getMacAddress())); |
| | | collector.setCollectorName(trimToNull(collector.getCollectorName())); |
| | | collector.setManufacturer(trimToNull(collector.getManufacturer())); |
| | | collector.setModel(trimToNull(collector.getModel())); |
| | |
| | | { |
| | | throw new ServiceException("采集器名称不能为空"); |
| | | } |
| | | validateIpAddress(collector.getIpAddress()); |
| | | validateOptionalIpAddress(collector.getIpAddress()); |
| | | if (collector.getChannelCount() == null || collector.getChannelCount() < 1 |
| | | || collector.getChannelCount() > 10000) |
| | | { |
| | |
| | | validateUnique(collector); |
| | | } |
| | | |
| | | /** 编辑时用既有值补齐未提交的字段,避免把"没传"当成"清空"。 */ |
| | | private void fillMissingFromCurrent(TCollector collector, TCollector current) |
| | | { |
| | | if (collector.getCollectorCode() == null) collector.setCollectorCode(current.getCollectorCode()); |
| | | if (collector.getIpAddress() == null) collector.setIpAddress(current.getIpAddress()); |
| | | if (collector.getMacAddress() == null) collector.setMacAddress(current.getMacAddress()); |
| | | if (collector.getCollectorName() == null) collector.setCollectorName(current.getCollectorName()); |
| | | if (collector.getChannelCount() == null) collector.setChannelCount(current.getChannelCount()); |
| | | if (collector.getStatus() == null) collector.setStatus(current.getStatus()); |
| | | if (collector.getAdapterId() == null) collector.setAdapterId(current.getAdapterId()); |
| | | } |
| | | |
| | | /** 校验所链接的来源实例存在;来源是采集器测量的提供方,链接后可读它的连接结果。 */ |
| | | private void requireLinkedAdapter(TCollector collector) |
| | | { |
| | | if (collector.getAdapterId() == null) |
| | | { |
| | | return; |
| | | } |
| | | TDeviceAdapter adapter = adapterMapper.selectDeviceAdapterById(collector.getAdapterId()); |
| | | if (adapter == null) |
| | | { |
| | | throw new ServiceException("所链接的数据来源不存在"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 校验设备编码和 MAC 地址在未删除数据中唯一。 |
| | | * 校验设备编码唯一;MAC 地址只在填写时参与唯一性。 |
| | | */ |
| | | private void validateUnique(TCollector collector) |
| | | { |
| | |
| | | if (isDifferentCollector(sameCode, collector.getCollectorId())) |
| | | { |
| | | throw new ServiceException("设备编码已存在"); |
| | | } |
| | | if (collector.getMacAddress() == null) |
| | | { |
| | | return; |
| | | } |
| | | TCollector sameMac = collectorMapper.selectCollectorByMacAddress(collector.getMacAddress()); |
| | | if (isDifferentCollector(sameMac, collector.getCollectorId())) |
| | |
| | | } |
| | | |
| | | /** |
| | | * 规范化并校验 MAC 地址。 |
| | | * 规范化并校验 MAC 地址;未填写时保持为空。 |
| | | */ |
| | | private String normalizeMacAddress(String macAddress) |
| | | private String normalizeOptionalMacAddress(String macAddress) |
| | | { |
| | | String normalized = trimToNull(macAddress); |
| | | if (normalized == null) |
| | | { |
| | | throw new ServiceException("MAC地址不能为空"); |
| | | return null; |
| | | } |
| | | normalized = normalized.replace('-', ':').toUpperCase(); |
| | | if (!MAC_PATTERN.matcher(normalized).matches()) |
| | |
| | | } |
| | | |
| | | /** |
| | | * 校验 IPv4 或 IPv6 地址,不接受主机名。 |
| | | * 校验 IPv4 或 IPv6 地址,不接受主机名;未填写时跳过。 |
| | | */ |
| | | private void validateIpAddress(String ipAddress) |
| | | private void validateOptionalIpAddress(String ipAddress) |
| | | { |
| | | if (StringUtils.isEmpty(ipAddress)) |
| | | { |
| | | throw new ServiceException("IP地址不能为空"); |
| | | return; |
| | | } |
| | | if (IPV4_PATTERN.matcher(ipAddress).matches()) |
| | | { |
| | |
| | | private String trimToNull(String value) |
| | | { |
| | | return StringUtils.isEmpty(StringUtils.trim(value)) ? null : StringUtils.trim(value); |
| | | } |
| | | |
| | | private Map<String, CollectorCandidate> candidatesByCode() |
| | | { |
| | | return candidateStore.snapshot(Instant.now()).collectors().stream() |
| | | .collect(java.util.stream.Collectors.toMap(CollectorCandidate::deviceCode, item -> item)); |
| | | } |
| | | |
| | | private TCollector applyRealtimeState(TCollector collector, Map<String, CollectorCandidate> candidates) |
| | | { |
| | | CollectorCandidate candidate = candidates.get(collector.getCollectorCode()); |
| | | collector.setOnlineStatus(candidate != null && candidate.online() ? ONLINE : OFFLINE); |
| | | // 候选过期只改变当前在线展示,不能抹掉 MySQL 保存的最近观测时间。 |
| | | if (candidate != null) collector.setLastSeenAtUtc(candidate.observedAt()); |
| | | return collector; |
| | | } |
| | | } |