|
|
@@ -8,11 +8,9 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.hdkj.lt.bf.application.FeederGroupApplication;
|
|
|
import com.hdkj.lt.bf.common.SeMonitorThreshold;
|
|
|
import com.hdkj.lt.bf.common.SeSnapshotConstants;
|
|
|
-import com.hdkj.lt.bf.entity.DwdShbDsCableSegmentBase;
|
|
|
-import com.hdkj.lt.bf.entity.DwdShbDsSpanSegmentBase;
|
|
|
import com.hdkj.lt.bf.entity.DwdShbDsTransformerBase;
|
|
|
import com.hdkj.lt.bf.entity.FhzgSeCurrentEvent;
|
|
|
-import com.hdkj.lt.bf.entity.FhzgSeLineLossDetail;
|
|
|
+import com.hdkj.lt.bf.entity.FhzgSeLineLossRate;
|
|
|
import com.hdkj.lt.bf.entity.FhzgSeMonitorCurrent;
|
|
|
import com.hdkj.lt.bf.entity.FhzgSeMonitorVoltage;
|
|
|
import com.hdkj.lt.bf.entity.FhzgSeSnapshotDetail;
|
|
|
@@ -21,13 +19,11 @@ import com.hdkj.lt.bf.event.MvtransOverVoltageEvent;
|
|
|
import com.hdkj.lt.bf.event.ReconTriggerEvent;
|
|
|
import com.hdkj.lt.bf.mapper.FeederTopoMapper;
|
|
|
import com.hdkj.lt.bf.mapper.FhzgSeCurrentEventMapper;
|
|
|
-import com.hdkj.lt.bf.mapper.FhzgSeLineLossDetailMapper;
|
|
|
+import com.hdkj.lt.bf.mapper.FhzgSeLineLossRateMapper;
|
|
|
import com.hdkj.lt.bf.mapper.FhzgSeMonitorCurrentMapper;
|
|
|
import com.hdkj.lt.bf.mapper.FhzgSeMonitorVoltageMapper;
|
|
|
import com.hdkj.lt.bf.mapper.FhzgSeSnapshotDetailMapper;
|
|
|
import com.hdkj.lt.bf.mapper.FhzgSeVoltageEventMapper;
|
|
|
-import com.hdkj.lt.bf.mapper.DwdShbDsCableSegmentBaseMapper;
|
|
|
-import com.hdkj.lt.bf.mapper.DwdShbDsSpanSegmentBaseMapper;
|
|
|
import com.hdkj.lt.bf.mapper.DwdShbDsTransformerBaseMapper;
|
|
|
import com.hdkj.lt.bf.service.SeSnapshotService;
|
|
|
import com.hdkj.lt.core.bizms.modle.po.FeederTopo;
|
|
|
@@ -59,9 +55,7 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
|
|
|
private final FeederGroupApplication feederGroupApplication;
|
|
|
private final ApplicationEventPublisher eventPublisher;
|
|
|
private final FeederTopoMapper feederTopoMapper;
|
|
|
- private final FhzgSeLineLossDetailMapper lineLossDetailMapper;
|
|
|
- private final DwdShbDsSpanSegmentBaseMapper spanSegmentMapper;
|
|
|
- private final DwdShbDsCableSegmentBaseMapper cableSegmentMapper;
|
|
|
+ private final FhzgSeLineLossRateMapper lineLossRateMapper;
|
|
|
|
|
|
private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
|
|
|
@@ -170,11 +164,10 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // === 3. 线路线段 → 线损计算 ===
|
|
|
- JSONArray segments = root.getJSONArray("periodSegmentSeResult");
|
|
|
- if (segments != null && !segments.isEmpty()) {
|
|
|
- processSegmentsForLineLoss(segments, snapTime, feederMap);
|
|
|
- }
|
|
|
+ // === 3. 馈线线损率(状估口径)===
|
|
|
+ // 线损率 = (总功率 - 配变有功 - 中压用户接入点有功) / 总功率 × 100%
|
|
|
+ // 总功率=馈线块 seoPs(≈出线开关功率);配变块=0302+0110;中压用户接入点块=370000
|
|
|
+ processLineLossRate(root, snapTime, feederMap, advanceState);
|
|
|
|
|
|
if (!batch.isEmpty()) {
|
|
|
// 防重:查该断面时刻已有的所有 device_id(精确到秒,无需再 IN 过滤)
|
|
|
@@ -209,6 +202,13 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
|
|
|
if (feederId == null || feederId.isEmpty()) return null; // 缺 psrId 直接丢弃,防 NULL 脏写
|
|
|
String[] loc = feederMap.getOrDefault(feederId, new String[]{"", "", ""});
|
|
|
BigDecimal loadRate = firstValue(f.getJSONArray("loadRates"));
|
|
|
+ // 负载率超过 200% 视为异常数据(状估量测异常),不计入计算:
|
|
|
+ // loadRate 置 null → 不触发电流事件、不参与负载率聚合统计
|
|
|
+ if (loadRate != null && loadRate.compareTo(SeMonitorThreshold.LOAD_RATE_ABNORMAL) > 0) {
|
|
|
+ log.warn("馈线负载率异常(>{}),不计入计算: feederId={}, loadRate={}",
|
|
|
+ SeMonitorThreshold.LOAD_RATE_ABNORMAL, feederId, loadRate);
|
|
|
+ loadRate = null;
|
|
|
+ }
|
|
|
BigDecimal voltage = firstValue(f.getJSONArray("seoUs"));
|
|
|
BigDecimal voltageRatio = voltage != null
|
|
|
? voltage.divide(SeMonitorThreshold.FEEDER_BASE_KV, 4, RoundingMode.HALF_UP) : null;
|
|
|
@@ -522,129 +522,118 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
|
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
|
- // 线路线损计算
|
|
|
+ // 馈线线损率计算(状估口径)
|
|
|
// ============================================================
|
|
|
|
|
|
/**
|
|
|
- * 处理线路线段断面数据,计算逐段线损功率并写入明细表
|
|
|
+ * 馈线线损率计算(新算法,替代旧的逐段电流法)
|
|
|
* <p>
|
|
|
- * line_loss_kW = 3000 × I_kA² × r_Ω_per_km × (L_m / 1000)
|
|
|
- * 电阻率: dxd(导线段)=0.4132Ω/km, 0201(电缆段)=0.313Ω/km
|
|
|
- * 段长度从 dwd_shb_ds_span_segment_base(导线段)/ dwd_shb_ds_cable_segment_base(电缆段)的 length 取(单位:米)
|
|
|
- * SE 返回的 seoIs 为安培,先 /1000 换算成 kA
|
|
|
+ * 线损率 = (总功率 - 配变有功 - 中压用户接入点有功) / 总功率 × 100%
|
|
|
+ * <ul>
|
|
|
+ * <li>总功率 = periodFeederSeResult[].seoPs(馈线状估有功 Σ,≈出线开关功率)</li>
|
|
|
+ * <li>配变有功 = periodMVTransSeResult[].seoPs(0302+0110 全部配变 Σ)</li>
|
|
|
+ * <li>中压用户接入点有功 = periodEnergyConsumerSeResult[].seoPs(370000 Σ)</li>
|
|
|
+ * </ul>
|
|
|
+ * 每断面每馈线一条写入 fhzg_se_line_loss_rate;总功率缺失/为 0 的馈线跳过(仅 log)。
|
|
|
+ * backfill(advanceState=false)同样计算写库,仅跳过状态机推进。
|
|
|
*/
|
|
|
- private void processSegmentsForLineLoss(JSONArray segments, LocalDateTime snapTime, Map<String, String[]> feederMap) {
|
|
|
- // 1. 按类型分组查段长度(档距段→span表,电缆段→cable表),过滤在运设备
|
|
|
- Map<String, BigDecimal> lengthMap = new HashMap<>();
|
|
|
+ private void processLineLossRate(JSONObject root, LocalDateTime snapTime,
|
|
|
+ Map<String, String[]> feederMap, boolean advanceState) {
|
|
|
try {
|
|
|
- List<String> spanIds = new ArrayList<>();
|
|
|
- List<String> cableIds = new ArrayList<>();
|
|
|
- for (int i = 0; i < segments.size(); i++) {
|
|
|
- JSONObject seg = segments.getJSONObject(i);
|
|
|
- String pid = seg.getString("psrId");
|
|
|
- String pt = seg.getString("psrType");
|
|
|
- if (pid == null) continue;
|
|
|
- if ("dxd".equals(pt)) spanIds.add(pid);
|
|
|
- else if ("0201".equals(pt)) cableIds.add(pid);
|
|
|
- }
|
|
|
- if (!spanIds.isEmpty()) {
|
|
|
- spanSegmentMapper.selectList(
|
|
|
- new LambdaQueryWrapper<DwdShbDsSpanSegmentBase>()
|
|
|
- .select(DwdShbDsSpanSegmentBase::getPsrId, DwdShbDsSpanSegmentBase::getLength)
|
|
|
- .in(DwdShbDsSpanSegmentBase::getPsrId, spanIds)
|
|
|
- .eq(DwdShbDsSpanSegmentBase::getPsrState, "20"))
|
|
|
- .forEach(r -> { if (r.getLength() != null) lengthMap.put(r.getPsrId(), r.getLength()); });
|
|
|
+ // 1. 配变有功:periodMVTransSeResult 按 feederId 汇总 seoPs
|
|
|
+ Map<String, BigDecimal> transPowerMap = new HashMap<>();
|
|
|
+ JSONArray mvtrans = root.getJSONArray("periodMVTransSeResult");
|
|
|
+ if (mvtrans != null) {
|
|
|
+ for (int i = 0; i < mvtrans.size(); i++) {
|
|
|
+ JSONObject m = mvtrans.getJSONObject(i);
|
|
|
+ String feederId = m.getString("feederId");
|
|
|
+ BigDecimal p = firstValue(m.getJSONArray("seoPs"));
|
|
|
+ if (feederId == null || p == null) continue;
|
|
|
+ transPowerMap.merge(feederId, p, BigDecimal::add);
|
|
|
+ }
|
|
|
}
|
|
|
- if (!cableIds.isEmpty()) {
|
|
|
- cableSegmentMapper.selectList(
|
|
|
- new LambdaQueryWrapper<DwdShbDsCableSegmentBase>()
|
|
|
- .select(DwdShbDsCableSegmentBase::getPsrId, DwdShbDsCableSegmentBase::getLength)
|
|
|
- .in(DwdShbDsCableSegmentBase::getPsrId, cableIds)
|
|
|
- .eq(DwdShbDsCableSegmentBase::getPsrState, "20"))
|
|
|
- .forEach(r -> { if (r.getLength() != null) lengthMap.put(r.getPsrId(), r.getLength()); });
|
|
|
+
|
|
|
+ // 2. 中压用户接入点有功:periodEnergyConsumerSeResult 按 feederId 汇总 seoPs
|
|
|
+ Map<String, BigDecimal> consumerPowerMap = new HashMap<>();
|
|
|
+ JSONArray consumers = root.getJSONArray("periodEnergyConsumerSeResult");
|
|
|
+ if (consumers != null) {
|
|
|
+ for (int i = 0; i < consumers.size(); i++) {
|
|
|
+ JSONObject c = consumers.getJSONObject(i);
|
|
|
+ String feederId = c.getString("feederId");
|
|
|
+ BigDecimal p = firstValue(c.getJSONArray("seoPs"));
|
|
|
+ if (feederId == null || p == null) continue;
|
|
|
+ consumerPowerMap.merge(feederId, p, BigDecimal::add);
|
|
|
+ }
|
|
|
}
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("查询段长度失败", e);
|
|
|
- }
|
|
|
|
|
|
- // 3. 逐段计算线损
|
|
|
- List<FhzgSeLineLossDetail> batch = new ArrayList<>();
|
|
|
- LocalDateTime now = LocalDateTime.now();
|
|
|
- for (int i = 0; i < segments.size(); i++) {
|
|
|
- JSONObject seg = segments.getJSONObject(i);
|
|
|
- String psrId = seg.getString("psrId");
|
|
|
- String psrType = seg.getString("psrType");
|
|
|
- String feederId = seg.getString("feederId");
|
|
|
- String[] loc = feederMap.getOrDefault(feederId, new String[]{"", "", ""});
|
|
|
-
|
|
|
- BigDecimal current = firstValue(seg.getJSONArray("seoIs"));
|
|
|
- if (current == null || current.compareTo(BigDecimal.ZERO) <= 0) continue;
|
|
|
-
|
|
|
- BigDecimal sbLength = lengthMap.get(psrId);
|
|
|
- if (sbLength == null || sbLength.compareTo(BigDecimal.ZERO) <= 0) continue;
|
|
|
-
|
|
|
- // 电阻率
|
|
|
- BigDecimal resistivity;
|
|
|
- if ("0201".equals(psrType)) {
|
|
|
- resistivity = new BigDecimal("0.313"); // 电缆段
|
|
|
- } else if ("dxd".equals(psrType)) {
|
|
|
- resistivity = new BigDecimal("0.4132"); // 导线段
|
|
|
- } else {
|
|
|
- log.debug("未知段类型跳过 psrType={}, psrId={}", psrType, psrId);
|
|
|
- continue;
|
|
|
+ // 3. 馈线块:总功率 + 逐馈线计算线损率
|
|
|
+ JSONArray feeders = root.getJSONArray("periodFeederSeResult");
|
|
|
+ if (feeders == null || feeders.isEmpty()) {
|
|
|
+ return;
|
|
|
}
|
|
|
+ List<FhzgSeLineLossRate> batch = new ArrayList<>();
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ for (int i = 0; i < feeders.size(); i++) {
|
|
|
+ JSONObject f = feeders.getJSONObject(i);
|
|
|
+ String feederId = f.getString("psrId");
|
|
|
+ if (feederId == null || feederId.isEmpty()) continue;
|
|
|
|
|
|
- // SE 返回电流为安培,换算成 kA:I_kA = I_A / 1000
|
|
|
-// BigDecimal currentKa = current.divide(BigDecimal.valueOf(1000), 8, RoundingMode.HALF_UP);
|
|
|
- BigDecimal currentKa = current.setScale(8, RoundingMode.HALF_UP);
|
|
|
-
|
|
|
- // 3000 × I_kA² × r × (L_m / 1000) = 3 × I_kA² × r × L_m
|
|
|
- BigDecimal lengthKm = sbLength.divide(BigDecimal.valueOf(1000), 8, RoundingMode.HALF_UP);
|
|
|
- BigDecimal loss = BigDecimal.valueOf(3000)
|
|
|
- .multiply(currentKa.pow(2))
|
|
|
- .multiply(resistivity)
|
|
|
- .multiply(lengthKm)
|
|
|
- .setScale(4, RoundingMode.HALF_UP);
|
|
|
-
|
|
|
- FhzgSeLineLossDetail detail = FhzgSeLineLossDetail.builder()
|
|
|
- .snapTime(snapTime)
|
|
|
- .feederId(feederId)
|
|
|
- .psrId(psrId)
|
|
|
- .psrType(psrType)
|
|
|
- .countyId(loc.length > 1 ? loc[1] : "")
|
|
|
- .subsId(loc.length > 0 ? loc[0] : "")
|
|
|
- .currentKa(currentKa)
|
|
|
- .sbLengthM(sbLength)
|
|
|
- .resistivity(resistivity)
|
|
|
- .lineLossKw(loss)
|
|
|
- .createTime(now)
|
|
|
- .build();
|
|
|
- batch.add(detail);
|
|
|
- }
|
|
|
+ BigDecimal totalPower = firstValue(f.getJSONArray("seoPs"));
|
|
|
+ if (totalPower == null || totalPower.compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ log.debug("馈线总功率缺失/为0, 跳过线损率: feederId={}", feederId);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- if (!batch.isEmpty()) {
|
|
|
- // 防重:查该断面时刻已有的段(避免重复跑同一断面时叠加)
|
|
|
- Set<String> existingPsrIds = new HashSet<>();
|
|
|
- lineLossDetailMapper.selectList(
|
|
|
- new LambdaQueryWrapper<FhzgSeLineLossDetail>()
|
|
|
- .select(FhzgSeLineLossDetail::getPsrId)
|
|
|
- .eq(FhzgSeLineLossDetail::getSnapTime, snapTime)
|
|
|
- ).forEach(e -> existingPsrIds.add(e.getPsrId()));
|
|
|
-
|
|
|
- List<FhzgSeLineLossDetail> toInsert = new ArrayList<>();
|
|
|
- for (FhzgSeLineLossDetail d : batch) {
|
|
|
- if (existingPsrIds.contains(d.getPsrId())) continue;
|
|
|
- toInsert.add(d);
|
|
|
+ BigDecimal transPower = transPowerMap.getOrDefault(feederId, BigDecimal.ZERO);
|
|
|
+ BigDecimal consumerPower = consumerPowerMap.getOrDefault(feederId, BigDecimal.ZERO);
|
|
|
+ BigDecimal lineLossPower = totalPower.subtract(transPower).subtract(consumerPower);
|
|
|
+ BigDecimal lineLossRate = lineLossPower.multiply(BigDecimal.valueOf(100))
|
|
|
+ .divide(totalPower, 4, RoundingMode.HALF_UP);
|
|
|
+
|
|
|
+ String[] loc = feederMap.getOrDefault(feederId, new String[]{"", "", ""});
|
|
|
+ batch.add(FhzgSeLineLossRate.builder()
|
|
|
+ .snapTime(snapTime)
|
|
|
+ .feederId(feederId)
|
|
|
+ .feederName(loc.length > 2 ? loc[2] : null)
|
|
|
+ .subsId(loc.length > 0 ? loc[0] : "")
|
|
|
+ .countyId(loc.length > 1 ? loc[1] : "")
|
|
|
+ .totalPower(totalPower)
|
|
|
+ .transPower(transPower)
|
|
|
+ .consumerPower(consumerPower)
|
|
|
+ .lineLossPower(lineLossPower)
|
|
|
+ .lineLossRate(lineLossRate)
|
|
|
+ .createTime(now)
|
|
|
+ .build());
|
|
|
}
|
|
|
|
|
|
- int inserted = 0;
|
|
|
- if (!toInsert.isEmpty()) {
|
|
|
- inserted = insertBatchSharded(rows -> lineLossDetailMapper.insertBatch(rows), toInsert);
|
|
|
+ if (!batch.isEmpty()) {
|
|
|
+ // 防重:查该断面时刻已有的馈线(避免重复跑同一断面时叠加)
|
|
|
+ Set<String> existingFeederIds = new HashSet<>();
|
|
|
+ lineLossRateMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<FhzgSeLineLossRate>()
|
|
|
+ .select(FhzgSeLineLossRate::getFeederId)
|
|
|
+ .eq(FhzgSeLineLossRate::getSnapTime, snapTime)
|
|
|
+ ).forEach(e -> existingFeederIds.add(e.getFeederId()));
|
|
|
+
|
|
|
+ List<FhzgSeLineLossRate> toInsert = new ArrayList<>();
|
|
|
+ for (FhzgSeLineLossRate d : batch) {
|
|
|
+ if (existingFeederIds.contains(d.getFeederId())) continue;
|
|
|
+ toInsert.add(d);
|
|
|
+ }
|
|
|
+
|
|
|
+ int inserted = 0;
|
|
|
+ if (!toInsert.isEmpty()) {
|
|
|
+ inserted = insertBatchSharded(rows -> {
|
|
|
+ toInsert.forEach(lineLossRateMapper::insert);
|
|
|
+ return toInsert.size();
|
|
|
+ }, toInsert);
|
|
|
+ }
|
|
|
+ log.info("馈线线损率计算完成 snapTime={}, 写入{}条, 跳过{}条(无功率{}条/重复{}条)",
|
|
|
+ snapTime, inserted, batch.size() - inserted,
|
|
|
+ batch.size() - toInsert.size(), existingFeederIds.size());
|
|
|
}
|
|
|
- int skippedNoData = segments.size() - batch.size();
|
|
|
- int skippedDup = batch.size() - toInsert.size();
|
|
|
- log.info("线路线损计算完成 snapTime={}, 写入{}条, 跳过{}条(无数据{}条/重复{}条)",
|
|
|
- snapTime, inserted, skippedNoData + skippedDup, skippedNoData, skippedDup);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("馈线线损率计算失败 snapTime={}", snapTime, e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -914,11 +903,11 @@ public class SeSnapshotServiceImpl implements SeSnapshotService {
|
|
|
int deleted = snapshotDetailMapper.delete(
|
|
|
new LambdaQueryWrapper<FhzgSeSnapshotDetail>()
|
|
|
.eq(FhzgSeSnapshotDetail::getSnapshotTime, snapTime));
|
|
|
- // 线损明细同步删除(否则防重会跳过插入,保留旧值导致与断面不一致)
|
|
|
- int deletedLoss = lineLossDetailMapper.delete(
|
|
|
- new LambdaQueryWrapper<FhzgSeLineLossDetail>()
|
|
|
- .eq(FhzgSeLineLossDetail::getSnapTime, snapTime));
|
|
|
- log.info("状估覆盖更新 snapTime={}, 删除断面{}条, 删除线损{}条", snapTime, deleted, deletedLoss);
|
|
|
+ // 线损率同步删除(否则防重会跳过插入,保留旧值导致与断面不一致)
|
|
|
+ int deletedLoss = lineLossRateMapper.delete(
|
|
|
+ new LambdaQueryWrapper<FhzgSeLineLossRate>()
|
|
|
+ .eq(FhzgSeLineLossRate::getSnapTime, snapTime));
|
|
|
+ log.info("状估覆盖更新 snapTime={}, 删除断面{}条, 删除线损率{}条", snapTime, deleted, deletedLoss);
|
|
|
|
|
|
// 重新走补数链路写入(此时防重逻辑不会跳过任何记录)
|
|
|
// 注意:覆盖更新用于补历史断面,只写数据不推状态机——防止状态机时间倒退、
|