package com.battery.business.service.impl;
|
|
import java.net.Inet6Address;
|
import java.net.InetAddress;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
import java.util.Set;
|
import java.time.Instant;
|
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import com.ruoyi.common.constant.UserConstants;
|
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.ip.IpUtils;
|
import com.battery.business.domain.TDetectionCabinet;
|
import com.battery.business.domain.TDetectionChannel;
|
import com.battery.business.domain.vo.DiscoveredCabinetVo;
|
import com.battery.business.mapper.TDetectionCabinetMapper;
|
import com.battery.business.service.IDeviceAdapterService;
|
import com.battery.business.service.ITDetectionCabinetService;
|
import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore;
|
import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate;
|
import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate;
|
import com.battery.device.api.model.DeviceWorkStatus;
|
|
/**
|
* 检测柜 Service 业务层处理
|
*
|
* @author ruoyi
|
*/
|
@Service
|
public class TDetectionCabinetServiceImpl implements ITDetectionCabinetService
|
{
|
private static final String NORMAL = "0";
|
private static final String DISABLED = "1";
|
private static final String TESTING = "1";
|
private static final String ONLINE = "0";
|
private static final String OFFLINE = "1";
|
|
private final TDetectionCabinetMapper detectionCabinetMapper;
|
private final DeviceDiscoveryCandidateStore candidateStore;
|
private final IDeviceAdapterService adapterService;
|
|
/**
|
* 构造检测柜业务服务。
|
*
|
* @param detectionCabinetMapper 检测柜数据访问对象
|
*/
|
public TDetectionCabinetServiceImpl(TDetectionCabinetMapper detectionCabinetMapper,
|
DeviceDiscoveryCandidateStore candidateStore)
|
{
|
this(detectionCabinetMapper, candidateStore, null);
|
}
|
|
/**
|
* 构造检测柜业务服务。
|
*
|
* @param detectionCabinetMapper 检测柜数据访问对象
|
* @param candidateStore 发现候选存储
|
* @param adapterService 适配器实例服务;为空时列表不展示数据来源名称
|
*/
|
@org.springframework.beans.factory.annotation.Autowired
|
public TDetectionCabinetServiceImpl(TDetectionCabinetMapper detectionCabinetMapper,
|
DeviceDiscoveryCandidateStore candidateStore,
|
@org.springframework.lang.Nullable IDeviceAdapterService adapterService)
|
{
|
this.detectionCabinetMapper = detectionCabinetMapper;
|
this.candidateStore = candidateStore;
|
this.adapterService = adapterService;
|
}
|
|
/** 数据来源名称按适配器编码解析;代码装配实例没有数据库记录时退回展示编码。 */
|
private Map<String, String> adapterNames()
|
{
|
if (adapterService == null) return Map.of();
|
return adapterService.selectList(new com.battery.business.domain.TDeviceAdapter()).stream()
|
.filter(item -> item.getAdapterCode() != null && item.getAdapterName() != null)
|
.collect(java.util.stream.Collectors.toMap(
|
com.battery.business.domain.TDeviceAdapter::getAdapterCode,
|
com.battery.business.domain.TDeviceAdapter::getAdapterName, (left, right) -> right));
|
}
|
|
/** 适配器编码可能缺失(历史登记),缺失时不查询来源名称。 */
|
private void applyAdapterName(TDetectionCabinet cabinet, Map<String, String> adapterNames)
|
{
|
if (cabinet == null || cabinet.getAdapterCode() == null) return;
|
cabinet.setAdapterName(adapterNames.get(cabinet.getAdapterCode()));
|
}
|
|
/**
|
* 查询指定检测柜,并在记录不存在或已逻辑删除时统一抛出业务异常。
|
*
|
* @param cabinetId 检测柜主键
|
* @return 检测柜信息
|
*/
|
@Override
|
public TDetectionCabinet selectDetectionCabinetById(Long cabinetId)
|
{
|
TDetectionCabinet detectionCabinet = detectionCabinetMapper.selectDetectionCabinetById(cabinetId);
|
if (StringUtils.isNull(detectionCabinet))
|
{
|
throw new ServiceException("检测柜不存在或已删除");
|
}
|
applyAdapterName(detectionCabinet, adapterNames());
|
return applyRealtimeState(detectionCabinet, candidatesByCode());
|
}
|
|
/**
|
* 查询检测柜列表;查询 MAC 地址时先转换为统一格式,避免分隔符和大小写导致漏查。
|
*
|
* @param detectionCabinet 查询条件
|
* @return 检测柜列表
|
*/
|
@Override
|
public List<TDetectionCabinet> selectDetectionCabinetList(TDetectionCabinet detectionCabinet)
|
{
|
String requestedOnlineStatus = detectionCabinet.getOnlineStatus();
|
String requestedTestStatus = detectionCabinet.getTestStatus();
|
if (StringUtils.isNotBlank(detectionCabinet.getMacAddress()))
|
{
|
detectionCabinet.setMacAddress(normalizeMacAddress(detectionCabinet.getMacAddress()));
|
}
|
detectionCabinet.setOnlineStatus(null);
|
detectionCabinet.setTestStatus(null);
|
Map<String, CabinetCandidate> candidates = candidatesByCode();
|
Map<String, String> adapterNames = adapterNames();
|
return detectionCabinetMapper.selectDetectionCabinetList(detectionCabinet).stream()
|
.map(item -> applyRealtimeState(item, candidates))
|
.peek(item -> applyAdapterName(item, adapterNames))
|
.filter(item -> StringUtils.isBlank(requestedOnlineStatus)
|
|| requestedOnlineStatus.equals(item.getOnlineStatus()))
|
.filter(item -> StringUtils.isBlank(requestedTestStatus)
|
|| requestedTestStatus.equals(item.getTestStatus()))
|
.toList();
|
}
|
|
/**
|
* 读取未登记且未删除的候选,离线设备仍保留身份并可选择。
|
* 候选不按时间过期(厂商存储里的通道登记是长期事实),只按"已登记且未删除"排除;
|
* 每条附带最近数据时间并按它倒序,便于操作员判断哪几台真的在产出数据。
|
* 发现只返回设备事实,不从通道工作状态推断系统测试关系,也不自动判定"在用"。
|
*
|
* @return 可供选择的检测柜设备摘要
|
*/
|
@Override
|
public List<DiscoveredCabinetVo> selectDiscoveredCabinetList()
|
{
|
Set<String> registeredCodes = Set.copyOf(detectionCabinetMapper.selectRegisteredCabinetCodes());
|
return candidateStore.snapshot(Instant.now()).cabinets().stream()
|
.filter(candidate -> !registeredCodes.contains(candidate.deviceCode()))
|
.map(candidate -> new DiscoveredCabinetVo(
|
candidate.deviceCode(), candidate.ip(), candidate.mac(), candidate.name(),
|
candidate.manufacturer(), candidate.model(), candidate.channels().size(),
|
candidate.online() ? "在线" : "离线", candidate.lastDataAt()))
|
.sorted(java.util.Comparator.comparing(DiscoveredCabinetVo::lastDataAt,
|
java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder()))
|
.thenComparing(DiscoveredCabinetVo::deviceCode))
|
.toList();
|
}
|
|
/**
|
* 将操作员选择的发现候选设备登记为检测柜,并保留其适配器和通道身份。
|
*
|
* @param detectionCabinet 检测柜信息
|
* @return 影响行数
|
*/
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int insertDetectionCabinet(TDetectionCabinet detectionCabinet)
|
{
|
normalizeBasicFields(detectionCabinet);
|
CabinetCandidate candidate = candidateStore.findCabinet(detectionCabinet.getCabinetCode(), Instant.now())
|
.orElseThrow(() -> new ServiceException("请先选择有效的发现候选设备"));
|
detectionCabinet.setAdapterId(candidate.adapterId());
|
detectionCabinet.setAdapterCode(candidate.adapterCode());
|
detectionCabinet.setVendorDeviceType(candidate.devtype());
|
detectionCabinet.setVendorDeviceId(candidate.devid());
|
detectionCabinet.setIpAddress(candidate.ip());
|
detectionCabinet.setMacAddress(candidate.mac() == null ? detectionCabinet.getMacAddress() : candidate.mac());
|
detectionCabinet.setChannelCount(candidate.channels().size());
|
validateIpAddress(detectionCabinet.getIpAddress());
|
validateUniqueFields(detectionCabinet);
|
detectionCabinet.setOnlineStatus("OFFLINE");
|
detectionCabinet.setLastSeenAtUtc(null);
|
detectionCabinet.setStatus(NORMAL);
|
try
|
{
|
int rows = detectionCabinetMapper.insertDetectionCabinet(detectionCabinet);
|
for (var channel : candidate.channels())
|
{
|
TDetectionChannel value = new TDetectionChannel();
|
value.setAdapterId(candidate.adapterId());
|
value.setAdapterCode(candidate.adapterCode());
|
value.setCabinetId(detectionCabinet.getCabinetId());
|
value.setDevtype(channel.identity().devtype());
|
value.setDevid(channel.identity().devid());
|
value.setSubdevid(channel.identity().subdevid());
|
value.setChlid(channel.identity().chlid());
|
value.setDisplayNo(channelDisplayNo(channel));
|
value.setOnlineStatus(channel.online() ? "ONLINE" : "OFFLINE");
|
value.setWorkStatus(persistedWorkStatus(channel.workStatus(), channel.online()));
|
value.setLastSeenAtUtc(null);
|
detectionCabinetMapper.insertDetectionChannel(value);
|
}
|
// 登记只确认来源身份,在线与离线事实都按候选原样同步。
|
detectionCabinetMapper.updateRegisteredCabinetObservation(candidate.adapterCode(), candidate.devtype(),
|
candidate.devid(), candidate.online() ? "ONLINE" : "OFFLINE", candidate.observedAt(),
|
candidate.online());
|
candidate.channels().forEach(channel -> detectionCabinetMapper.updateRegisteredChannelObservation(
|
candidate.adapterCode(), channel.identity().devtype(), channel.identity().devid(),
|
channel.identity().subdevid(), channel.identity().chlid(), channelDisplayNo(channel),
|
channel.online() ? "ONLINE" : "OFFLINE",
|
channel.online() ? persistedWorkStatus(channel.workStatus(), true) : "UNOBSERVABLE",
|
channel.observedAt(), channel.online()));
|
return rows;
|
}
|
catch (DuplicateKeyException exception)
|
{
|
throw buildDuplicateException(detectionCabinet, exception);
|
}
|
}
|
|
/**
|
* 修改检测柜基础信息;先确认目标记录存在,再执行字段规范化和唯一性校验。
|
*
|
* @param detectionCabinet 检测柜信息
|
* @return 影响行数
|
*/
|
@Override
|
public int updateDetectionCabinet(TDetectionCabinet detectionCabinet)
|
{
|
requireCabinetId(detectionCabinet.getCabinetId());
|
selectDetectionCabinetById(detectionCabinet.getCabinetId());
|
normalizeBasicFields(detectionCabinet);
|
validateIpAddress(detectionCabinet.getIpAddress());
|
validateUniqueFields(detectionCabinet);
|
try
|
{
|
return detectionCabinetMapper.updateDetectionCabinet(detectionCabinet);
|
}
|
catch (DuplicateKeyException exception)
|
{
|
throw buildDuplicateException(detectionCabinet, exception);
|
}
|
}
|
|
/**
|
* 修改检测柜启禁用状态;测试中的检测柜禁止被禁用,重复状态更新直接返回成功。
|
*
|
* @param detectionCabinet 检测柜主键、目标状态及更新人
|
* @return 影响行数
|
*/
|
@Override
|
public int updateDetectionCabinetStatus(TDetectionCabinet detectionCabinet)
|
{
|
requireCabinetId(detectionCabinet.getCabinetId());
|
if (!NORMAL.equals(detectionCabinet.getStatus()) && !DISABLED.equals(detectionCabinet.getStatus()))
|
{
|
throw new ServiceException("检测柜状态值不正确");
|
}
|
TDetectionCabinet current = selectDetectionCabinetById(detectionCabinet.getCabinetId());
|
if (DISABLED.equals(detectionCabinet.getStatus()))
|
{
|
validateCabinetNotTesting(current, "禁用");
|
}
|
if (detectionCabinet.getStatus().equals(current.getStatus()))
|
{
|
return 1;
|
}
|
return detectionCabinetMapper.updateDetectionCabinetStatus(detectionCabinet);
|
}
|
|
/**
|
* 批量取消检测柜登记;先完成全部目标记录校验,再执行操作,避免部分成功。
|
*
|
* @param cabinetIds 检测柜主键数组
|
* @param updateBy 更新人
|
* @return 影响行数
|
*/
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int deleteDetectionCabinetByIds(Long[] cabinetIds, String updateBy)
|
{
|
if (StringUtils.isEmpty(cabinetIds))
|
{
|
throw new ServiceException("请选择要删除的检测柜");
|
}
|
for (Long cabinetId : cabinetIds)
|
{
|
TDetectionCabinet detectionCabinet = selectDetectionCabinetById(cabinetId);
|
validateCabinetNotTesting(detectionCabinet, "删除");
|
}
|
int rows = 0;
|
for (Long cabinetId : cabinetIds)
|
{
|
detectionCabinetMapper.deleteDetectionChannelsByCabinetId(cabinetId);
|
rows += detectionCabinetMapper.deleteDetectionCabinetById(cabinetId, updateBy);
|
}
|
return rows;
|
}
|
|
/**
|
* 校验设备编码唯一性,编辑当前记录时允许命中自身。
|
*
|
* @param detectionCabinet 待校验检测柜
|
* @return true 表示唯一
|
*/
|
@Override
|
public boolean checkCabinetCodeUnique(TDetectionCabinet detectionCabinet)
|
{
|
Long cabinetId = StringUtils.isNull(detectionCabinet.getCabinetId()) ? -1L : detectionCabinet.getCabinetId();
|
TDetectionCabinet info = detectionCabinetMapper.selectByCabinetCode(detectionCabinet.getCabinetCode());
|
if (StringUtils.isNotNull(info) && info.getCabinetId().longValue() != cabinetId.longValue())
|
{
|
return UserConstants.NOT_UNIQUE;
|
}
|
return UserConstants.UNIQUE;
|
}
|
|
/**
|
* 校验 MAC 地址唯一性,调用方应传入规范化后的地址。
|
*
|
* @param detectionCabinet 待校验检测柜
|
* @return true 表示唯一
|
*/
|
@Override
|
public boolean checkMacAddressUnique(TDetectionCabinet detectionCabinet)
|
{
|
if (StringUtils.isBlank(detectionCabinet.getMacAddress()))
|
{
|
return UserConstants.UNIQUE;
|
}
|
Long cabinetId = StringUtils.isNull(detectionCabinet.getCabinetId()) ? -1L : detectionCabinet.getCabinetId();
|
TDetectionCabinet info = detectionCabinetMapper.selectByMacAddress(detectionCabinet.getMacAddress());
|
if (StringUtils.isNotNull(info) && info.getCabinetId().longValue() != cabinetId.longValue())
|
{
|
return UserConstants.NOT_UNIQUE;
|
}
|
return UserConstants.UNIQUE;
|
}
|
|
private void validateUniqueFields(TDetectionCabinet detectionCabinet)
|
{
|
if (!checkCabinetCodeUnique(detectionCabinet))
|
{
|
throw new ServiceException("设备编码已存在");
|
}
|
if (!checkMacAddressUnique(detectionCabinet))
|
{
|
throw new ServiceException("MAC地址已存在");
|
}
|
}
|
|
private void normalizeBasicFields(TDetectionCabinet detectionCabinet)
|
{
|
detectionCabinet.setCabinetCode(StringUtils.trim(detectionCabinet.getCabinetCode()));
|
detectionCabinet.setCabinetName(StringUtils.trim(detectionCabinet.getCabinetName()));
|
detectionCabinet.setIpAddress(StringUtils.trim(detectionCabinet.getIpAddress()));
|
detectionCabinet.setMacAddress(normalizeMacAddress(detectionCabinet.getMacAddress()));
|
detectionCabinet.setManufacturer(StringUtils.trim(detectionCabinet.getManufacturer()));
|
detectionCabinet.setModel(StringUtils.trim(detectionCabinet.getModel()));
|
detectionCabinet.setRemark(StringUtils.trim(detectionCabinet.getRemark()));
|
}
|
|
private String normalizeMacAddress(String macAddress)
|
{
|
String normalized = StringUtils.trim(macAddress);
|
return StringUtils.isBlank(normalized) ? null : normalized.replace('-', ':').toUpperCase(Locale.ROOT);
|
}
|
|
private void validateIpAddress(String ipAddress)
|
{
|
if (IpUtils.isIP(ipAddress))
|
{
|
return;
|
}
|
try
|
{
|
InetAddress address = InetAddress.getByName(ipAddress);
|
if (ipAddress.contains(":") && address instanceof Inet6Address)
|
{
|
return;
|
}
|
}
|
catch (Exception ignored)
|
{
|
}
|
throw new ServiceException("IP地址格式不正确");
|
}
|
|
/**
|
* 校验检测柜是否处于可操作状态。
|
*
|
* @param detectionCabinet 检测柜当前数据
|
* @param operation 业务操作名称
|
*/
|
private void validateCabinetNotTesting(TDetectionCabinet detectionCabinet, String operation)
|
{
|
if (TESTING.equals(detectionCabinet.getTestStatus()))
|
{
|
throw new ServiceException("检测柜正在测试,不能" + operation);
|
}
|
}
|
|
private void requireCabinetId(Long cabinetId)
|
{
|
if (StringUtils.isNull(cabinetId))
|
{
|
throw new ServiceException("检测柜ID不能为空");
|
}
|
}
|
|
private Map<String, CabinetCandidate> candidatesByCode()
|
{
|
return candidateStore.snapshot(Instant.now()).cabinets().stream().collect(java.util.stream.Collectors.toMap(
|
item -> stableCandidateKey(item.adapterCode(), item.devtype(), item.devid()), item -> item,
|
(left, right) -> right));
|
}
|
|
private TDetectionCabinet applyRealtimeState(TDetectionCabinet cabinet,
|
Map<String, CabinetCandidate> candidates)
|
{
|
CabinetCandidate candidate = candidates.get(stableCandidateKey(cabinet.getAdapterCode(),
|
cabinet.getVendorDeviceType(), cabinet.getVendorDeviceId()));
|
if (candidate == null) candidate = candidateStore.findCabinet(cabinet.getCabinetCode(), Instant.now()).orElse(null);
|
cabinet.setOnlineStatus(candidate != null && candidate.online() ? ONLINE : OFFLINE);
|
return cabinet;
|
}
|
|
private String stableCandidateKey(String adapterCode, Integer devtype, Integer devid)
|
{
|
return adapterCode + "-" + devtype + "-" + devid;
|
}
|
|
/**
|
* 通道在界面上的显示号:来源自己有叫法就用它(操作员拿它跟厂商软件对上),
|
* 否则回退到身份里的「单元-通道」。显示号只是展示,不参与通道身份。
|
*/
|
static String channelDisplayNo(ChannelCandidate channel)
|
{
|
String declared = channel.sourceDisplayNo();
|
return declared == null || declared.isBlank()
|
? channel.identity().subdevid() + "-" + channel.identity().chlid() : declared.trim();
|
}
|
|
/** 将设备观测状态转换为最终数据库允许的通道事实状态。 */
|
private String persistedWorkStatus(DeviceWorkStatus status, boolean online)
|
{
|
if (!online || status == null || status == DeviceWorkStatus.UNKNOWN
|
|| status == DeviceWorkStatus.UNOBSERVABLE) return "UNOBSERVABLE";
|
return switch (status)
|
{
|
case PAUSED -> "PAUSED";
|
case PROTECTED -> "ABNORMAL";
|
case FINISHED, STOPPED -> "ENDED";
|
default -> "RUNNING";
|
};
|
}
|
|
private ServiceException buildDuplicateException(TDetectionCabinet detectionCabinet, DuplicateKeyException cause)
|
{
|
String message = checkCabinetCodeUnique(detectionCabinet) ? "MAC地址已存在" : "设备编码已存在";
|
return new ServiceException(message).setDetailMessage(cause.getMessage());
|
}
|
}
|