|
@@ -0,0 +1,249 @@
|
|
|
|
|
+package com.zksy.cockpit.relation;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.zksy.base.domain.EquipmentBase;
|
|
|
|
|
+import com.zksy.base.domain.GasPipePoint;
|
|
|
|
|
+import com.zksy.base.domain.GasPipePointDeviceRel;
|
|
|
|
|
+import com.zksy.base.domain.ManholeBase;
|
|
|
|
|
+import com.zksy.base.domain.ManholeDeviceRel;
|
|
|
|
|
+import com.zksy.base.domain.PipeNetworkBase;
|
|
|
|
|
+import com.zksy.base.mapper.GasPipePointDeviceRelMapper;
|
|
|
|
|
+import com.zksy.base.mapper.ManholeDeviceRelMapper;
|
|
|
|
|
+import com.zksy.base.service.EquipmentBaseService;
|
|
|
|
|
+import com.zksy.base.service.GasPipePointService;
|
|
|
|
|
+import com.zksy.base.service.ManholeBaseService;
|
|
|
|
|
+import com.zksy.base.service.PipeNetworkBaseService;
|
|
|
|
|
+import com.zksy.cockpit.foundation.CockpitReadException;
|
|
|
|
|
+import com.zksy.cockpit.relation.CockpitDeviceFacilityRelationResponse.Relation;
|
|
|
|
|
+import com.zksy.cockpit.relation.CockpitDeviceFacilityRelationResponse.Statistics;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.time.Clock;
|
|
|
|
|
+import java.time.ZoneId;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Comparator;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.HashSet;
|
|
|
|
|
+import java.util.LinkedHashSet;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.Set;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 设备 → 设施关联只读服务。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>数据来源</b>(只读查询,不写库、不推导):
|
|
|
|
|
+ * <ol>
|
|
|
|
|
+ * <li>窨井:{@code manhole_device_rel.manhole_id} → {@code manhole_base.manhole_id},设施主键即 manholeId;</li>
|
|
|
|
|
+ * <li>燃气管点:{@code gas_pipe_point_device_rel.point_id} → {@code gas_pipe_point.network_id}
|
|
|
|
|
+ * → {@code pipe_network_base.network_id},返回**管段**主键(前端挂的是管线);</li>
|
|
|
|
|
+ * <li>设备编码:{@code equipment_base.equipment_code}(按 rel.equipment_id 精确匹配主键)。</li>
|
|
|
|
|
+ * </ol>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>不编造</b>:只用上表主键做等值解析;解析不到就跳过该条并在 {@link Statistics} 里记缺,
|
|
|
|
|
+ * 既不按名称/编码猜设施,也不把查不到设备的行填个假 deviceCode。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>降级语义</b>:一条都解析不到 → {@code relations} 为空数组(正常响应);查询失败 → 抛
|
|
|
|
|
+ * {@link CockpitReadException},由控制器转 500,不把失败伪装成「无关联」。
|
|
|
|
|
+ */
|
|
|
|
|
+@Service
|
|
|
|
|
+public class CockpitDeviceFacilityRelationService {
|
|
|
|
|
+ private static final Logger LOG = LoggerFactory.getLogger(CockpitDeviceFacilityRelationService.class);
|
|
|
|
|
+
|
|
|
|
|
+ private static final String SCHEMA_TIME_ZONE = "Asia/Shanghai";
|
|
|
|
|
+ private static final String FACILITY_KIND_MANHOLE = "MANHOLE";
|
|
|
|
|
+ private static final String FACILITY_KIND_PIPE = "PIPE";
|
|
|
|
|
+ private static final String RELATION_TYPE_MANHOLE_DEVICE = "MANHOLE_DEVICE";
|
|
|
|
|
+ private static final String RELATION_TYPE_GAS_PIPE_POINT_DEVICE = "GAS_PIPE_POINT_DEVICE";
|
|
|
|
|
+
|
|
|
|
|
+ private final ManholeDeviceRelMapper manholeRelationMapper;
|
|
|
|
|
+ private final GasPipePointDeviceRelMapper gasPipePointRelationMapper;
|
|
|
|
|
+ private final ManholeBaseService manholes;
|
|
|
|
|
+ private final GasPipePointService gasPipePoints;
|
|
|
|
|
+ private final PipeNetworkBaseService pipeNetworks;
|
|
|
|
|
+ private final EquipmentBaseService equipments;
|
|
|
|
|
+ private final Clock clock;
|
|
|
|
|
+
|
|
|
|
|
+ public CockpitDeviceFacilityRelationService(ManholeDeviceRelMapper manholeRelationMapper,
|
|
|
|
|
+ GasPipePointDeviceRelMapper gasPipePointRelationMapper, ManholeBaseService manholes,
|
|
|
|
|
+ GasPipePointService gasPipePoints, PipeNetworkBaseService pipeNetworks, EquipmentBaseService equipments,
|
|
|
|
|
+ Clock clock) {
|
|
|
|
|
+ this.manholeRelationMapper = manholeRelationMapper;
|
|
|
|
|
+ this.gasPipePointRelationMapper = gasPipePointRelationMapper;
|
|
|
|
|
+ this.manholes = manholes;
|
|
|
|
|
+ this.gasPipePoints = gasPipePoints;
|
|
|
|
|
+ this.pipeNetworks = pipeNetworks;
|
|
|
|
|
+ this.equipments = equipments;
|
|
|
|
|
+ this.clock = clock;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public CockpitDeviceFacilityRelationResponse load() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // del_flag 显式写进条件:即使全局逻辑删除配置漂移,也只会读到未删除的关联行。
|
|
|
|
|
+ List<ManholeDeviceRel> manholeRelations = manholeRelationMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<ManholeDeviceRel>().eq(ManholeDeviceRel::getDelFlag, "0"));
|
|
|
|
|
+ List<GasPipePointDeviceRel> gasRelations = gasPipePointRelationMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<GasPipePointDeviceRel>().eq(GasPipePointDeviceRel::getDelFlag, "0"));
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, String> deviceCodeById = loadDeviceCodes(manholeRelations, gasRelations);
|
|
|
|
|
+ Set<String> existingManholeIds = loadExistingManholeIds(manholeRelations);
|
|
|
|
|
+ Map<Long, String> networkIdByPointId = loadNetworkIdByPointId(gasRelations);
|
|
|
|
|
+ Set<String> existingNetworkIds = loadExistingNetworkIds(networkIdByPointId.values());
|
|
|
|
|
+
|
|
|
|
|
+ List<Relation> relations = new ArrayList<>();
|
|
|
|
|
+ int skippedManholeMissing = 0;
|
|
|
|
|
+ int skippedGasNetworkMissing = 0;
|
|
|
|
|
+ int skippedDeviceMissing = 0;
|
|
|
|
|
+ int deviceCodeUnresolved = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (ManholeDeviceRel relation : manholeRelations) {
|
|
|
|
|
+ String deviceId = trimToNull(relation.getEquipmentId());
|
|
|
|
|
+ String manholeId = trimToNull(relation.getManholeId());
|
|
|
|
|
+ if (manholeId == null || !existingManholeIds.contains(manholeId)) {
|
|
|
|
|
+ skippedManholeMissing++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (deviceId == null) {
|
|
|
|
|
+ skippedDeviceMissing++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String deviceCode = trimToNull(deviceCodeById.get(deviceId));
|
|
|
|
|
+ if (deviceCode == null) {
|
|
|
|
|
+ deviceCodeUnresolved++;
|
|
|
|
|
+ }
|
|
|
|
|
+ relations.add(new Relation(deviceId, deviceCode, manholeId, FACILITY_KIND_MANHOLE,
|
|
|
|
|
+ RELATION_TYPE_MANHOLE_DEVICE));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (GasPipePointDeviceRel relation : gasRelations) {
|
|
|
|
|
+ String deviceId = trimToNull(relation.getEquipmentId());
|
|
|
|
|
+ String networkId = trimToNull(networkIdByPointId.get(relation.getPointId()));
|
|
|
|
|
+ if (networkId == null || !existingNetworkIds.contains(networkId)) {
|
|
|
|
|
+ skippedGasNetworkMissing++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (deviceId == null) {
|
|
|
|
|
+ skippedDeviceMissing++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String deviceCode = trimToNull(deviceCodeById.get(deviceId));
|
|
|
|
|
+ if (deviceCode == null) {
|
|
|
|
|
+ deviceCodeUnresolved++;
|
|
|
|
|
+ }
|
|
|
|
|
+ relations.add(new Relation(deviceId, deviceCode, networkId, FACILITY_KIND_PIPE,
|
|
|
|
|
+ RELATION_TYPE_GAS_PIPE_POINT_DEVICE));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 顺序固定,便于前端 diff / 排查时不因 SQL 返回顺序抖动。
|
|
|
|
|
+ relations.sort(Comparator.comparing(Relation::getFacilityKind)
|
|
|
|
|
+ .thenComparing(Relation::getFacilityCode)
|
|
|
|
|
+ .thenComparing(Relation::getDeviceId));
|
|
|
|
|
+
|
|
|
|
|
+ Statistics statistics = new Statistics(manholeRelations.size(), gasRelations.size(), relations.size(),
|
|
|
|
|
+ skippedManholeMissing, skippedGasNetworkMissing, skippedDeviceMissing, deviceCodeUnresolved);
|
|
|
|
|
+ String generatedAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME
|
|
|
|
|
+ .format(clock.instant().atZone(ZoneId.of(SCHEMA_TIME_ZONE)));
|
|
|
|
|
+ return new CockpitDeviceFacilityRelationResponse(generatedAt, true, relations, statistics);
|
|
|
|
|
+ } catch (RuntimeException exception) {
|
|
|
|
|
+ // 失败不能退化成空数组:空数组在前端代表「确实没有真实关联」。
|
|
|
|
|
+ LOG.error("Device facility relation source read failed", exception);
|
|
|
|
|
+ throw new CockpitReadException("device facility relation source unavailable");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 设备主键 → 设备编码;只做等值匹配,不按名称/编码猜。 */
|
|
|
|
|
+ private Map<String, String> loadDeviceCodes(List<ManholeDeviceRel> manholeRelations,
|
|
|
|
|
+ List<GasPipePointDeviceRel> gasRelations) {
|
|
|
|
|
+ Set<String> deviceIds = new LinkedHashSet<>();
|
|
|
|
|
+ for (ManholeDeviceRel relation : manholeRelations) {
|
|
|
|
|
+ addIfPresent(deviceIds, relation.getEquipmentId());
|
|
|
|
|
+ }
|
|
|
|
|
+ for (GasPipePointDeviceRel relation : gasRelations) {
|
|
|
|
|
+ addIfPresent(deviceIds, relation.getEquipmentId());
|
|
|
|
|
+ }
|
|
|
|
|
+ Map<String, String> deviceCodeById = new HashMap<>();
|
|
|
|
|
+ if (deviceIds.isEmpty()) {
|
|
|
|
|
+ return deviceCodeById;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (EquipmentBase equipment : equipments.listByIds(deviceIds)) {
|
|
|
|
|
+ if (equipment != null && equipment.getEquipmentId() != null) {
|
|
|
|
|
+ deviceCodeById.put(equipment.getEquipmentId(), equipment.getEquipmentCode());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return deviceCodeById;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 只认 manhole_base 里真实存在(未删除)的窨井主键。 */
|
|
|
|
|
+ private Set<String> loadExistingManholeIds(List<ManholeDeviceRel> manholeRelations) {
|
|
|
|
|
+ Set<String> manholeIds = new LinkedHashSet<>();
|
|
|
|
|
+ for (ManholeDeviceRel relation : manholeRelations) {
|
|
|
|
|
+ addIfPresent(manholeIds, relation.getManholeId());
|
|
|
|
|
+ }
|
|
|
|
|
+ Set<String> existing = new HashSet<>();
|
|
|
|
|
+ if (manholeIds.isEmpty()) {
|
|
|
|
|
+ return existing;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (ManholeBase manhole : manholes.listByIds(manholeIds)) {
|
|
|
|
|
+ if (manhole != null && manhole.getManholeId() != null) {
|
|
|
|
|
+ existing.add(manhole.getManholeId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return existing;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 管点主键 → 所属管段 networkId(管点表自身字段,原样读出)。 */
|
|
|
|
|
+ private Map<Long, String> loadNetworkIdByPointId(List<GasPipePointDeviceRel> gasRelations) {
|
|
|
|
|
+ Set<Long> pointIds = new LinkedHashSet<>();
|
|
|
|
|
+ for (GasPipePointDeviceRel relation : gasRelations) {
|
|
|
|
|
+ if (relation.getPointId() != null) {
|
|
|
|
|
+ pointIds.add(relation.getPointId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ Map<Long, String> networkIdByPointId = new HashMap<>();
|
|
|
|
|
+ if (pointIds.isEmpty()) {
|
|
|
|
|
+ return networkIdByPointId;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (GasPipePoint point : gasPipePoints.listByIds(pointIds)) {
|
|
|
|
|
+ if (point != null && point.getPointId() != null) {
|
|
|
|
|
+ networkIdByPointId.put(point.getPointId(), point.getNetworkId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return networkIdByPointId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 只认 pipe_network_base 里真实存在(未删除)的管段主键。 */
|
|
|
|
|
+ private Set<String> loadExistingNetworkIds(Iterable<String> networkIds) {
|
|
|
|
|
+ Set<String> candidates = new LinkedHashSet<>();
|
|
|
|
|
+ for (String networkId : networkIds) {
|
|
|
|
|
+ addIfPresent(candidates, networkId);
|
|
|
|
|
+ }
|
|
|
|
|
+ Set<String> existing = new HashSet<>();
|
|
|
|
|
+ if (candidates.isEmpty()) {
|
|
|
|
|
+ return existing;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (PipeNetworkBase network : pipeNetworks.listByIds(candidates)) {
|
|
|
|
|
+ if (network != null && network.getNetworkId() != null) {
|
|
|
|
|
+ existing.add(network.getNetworkId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return existing;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void addIfPresent(Set<String> target, String value) {
|
|
|
|
|
+ String trimmed = trimToNull(value);
|
|
|
|
|
+ if (trimmed != null) {
|
|
|
|
|
+ target.add(trimmed);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String trimToNull(String value) {
|
|
|
|
|
+ if (value == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String trimmed = value.trim();
|
|
|
|
|
+ return trimmed.isEmpty() ? null : trimmed;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|