feat(wm-dma): #59 DMA分区计量与漏损分析

- 新增 wm-dma 模块
- Entity: DmaZone, DmaMeter, DmaFlowRecord, DmaLeakageAnalysis, WaterBalance
- Mapper + Service + Controller(18个端点)
- DMA分区管理: 分区层级定义(CRUD) + 区域划分 + 关联设备 + 树形结构
- 分区计量: 各分区进出水量汇总 + 最小夜间流量(MNF)分析 + 流量趋势
- 漏损分析: 产销差计算 + 漏损率评估 + 趋势分析 + 报警
- 水平衡表: 供水量/售水量/漏损量平衡分析 + IWA组成
- DDL: dma_ddl.sql
- 单元测试: 5个Service测试类(25+测试用例)
This commit is contained in:
2026-06-14 16:06:49 +08:00
parent 08649f027e
commit f240059d1e
31 changed files with 2285 additions and 0 deletions
@@ -0,0 +1,130 @@
package com.water.dma.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.dma.entity.DmaFlowRecord;
import com.water.dma.mapper.DmaFlowRecordMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* DMA流量服务测试
*/
@ExtendWith(MockitoExtension.class)
class DmaFlowServiceTest {
@Mock
private DmaFlowRecordMapper flowRecordMapper;
private DmaFlowService flowService;
@BeforeEach
void setUp() {
flowService = new DmaFlowService(flowRecordMapper);
}
@Test
@DisplayName("创建流量记录")
void testCreate() {
DmaFlowRecord record = new DmaFlowRecord();
record.setZoneId(1L);
record.setMeterId(1L);
record.setInstantFlow(new BigDecimal("12.5"));
record.setTotalFlow(new BigDecimal("1000"));
record.setCollectTime(LocalDateTime.now());
when(flowRecordMapper.insert(any(DmaFlowRecord.class))).thenReturn(1);
DmaFlowRecord created = flowService.create(record);
assertNotNull(created);
assertEquals("good", created.getDataQuality());
verify(flowRecordMapper).insert(any(DmaFlowRecord.class));
}
@Test
@DisplayName("批量创建流量记录")
void testBatchCreate() {
DmaFlowRecord r1 = new DmaFlowRecord();
r1.setZoneId(1L);
r1.setMeterId(1L);
r1.setInstantFlow(new BigDecimal("10"));
r1.setCollectTime(LocalDateTime.now());
DmaFlowRecord r2 = new DmaFlowRecord();
r2.setZoneId(1L);
r2.setMeterId(2L);
r2.setInstantFlow(new BigDecimal("15"));
r2.setCollectTime(LocalDateTime.now());
when(flowRecordMapper.insert(any(DmaFlowRecord.class))).thenReturn(1);
flowService.batchCreate(Arrays.asList(r1, r2));
verify(flowRecordMapper, times(2)).insert(any(DmaFlowRecord.class));
}
@Test
@DisplayName("获取分区进出水量汇总")
void testGetZoneFlowSummary() {
Map<String, Object> row = new HashMap<>();
row.put("meter_id", 1L);
row.put("total_flow", new BigDecimal("100"));
when(flowRecordMapper.sumFlowByMeter(eq(1L), any(), any())).thenReturn(List.of(row));
Map<String, Object> summary = flowService.getZoneFlowSummary(1L,
LocalDateTime.now().minusHours(24), LocalDateTime.now());
assertNotNull(summary);
assertEquals(1L, summary.get("zoneId"));
assertEquals(1, summary.get("meterCount"));
}
@Test
@DisplayName("MNF分析-正常")
void testGetMNFAnalysis() {
when(flowRecordMapper.getMNF(eq(1L), any())).thenReturn(new BigDecimal("3.5"));
Map<String, Object> result = flowService.getMNFAnalysis(1L, LocalDate.now());
assertNotNull(result);
assertEquals(new BigDecimal("3.5"), result.get("mnf"));
assertEquals("正常", result.get("analysisResult"));
}
@Test
@DisplayName("MNF分析-无数据")
void testGetMNFAnalysisNoData() {
when(flowRecordMapper.getMNF(eq(1L), any())).thenReturn(null);
Map<String, Object> result = flowService.getMNFAnalysis(1L, LocalDate.now());
assertEquals(BigDecimal.ZERO, result.get("mnf"));
}
@Test
@DisplayName("获取流量趋势")
void testGetFlowTrend() {
DmaFlowRecord record = new DmaFlowRecord();
record.setCollectTime(LocalDateTime.now());
record.setInstantFlow(new BigDecimal("12.5"));
when(flowRecordMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(record));
List<Map<String, Object>> trend = flowService.getFlowTrend(1L,
LocalDateTime.now().minusHours(24), LocalDateTime.now());
assertEquals(1, trend.size());
}
}
@@ -0,0 +1,124 @@
package com.water.dma.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.dma.entity.DmaLeakageAnalysis;
import com.water.dma.entity.DmaZone;
import com.water.dma.mapper.DmaLeakageAnalysisMapper;
import com.water.dma.mapper.DmaZoneMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* DMA漏损分析服务测试
*/
@ExtendWith(MockitoExtension.class)
class DmaLeakageServiceTest {
@Mock
private DmaLeakageAnalysisMapper leakageMapper;
@Mock
private DmaZoneMapper zoneMapper;
private DmaLeakageService leakageService;
@BeforeEach
void setUp() {
leakageService = new DmaLeakageService(leakageMapper, zoneMapper);
}
@Test
@DisplayName("执行漏损分析-正常")
void testAnalyze() {
BigDecimal supply = new BigDecimal("1000");
BigDecimal sale = new BigDecimal("850");
when(leakageMapper.insert(any(DmaLeakageAnalysis.class))).thenReturn(1);
DmaLeakageAnalysis result = leakageService.analyze(1L, LocalDate.now(), supply, sale);
assertNotNull(result);
assertEquals(new BigDecimal("150"), result.getLeakageVolume());
assertEquals(0, new BigDecimal("15.00").compareTo(result.getNrwRate()));
assertEquals("warning", result.getAlarmLevel());
verify(leakageMapper).insert(any(DmaLeakageAnalysis.class));
}
@Test
@DisplayName("执行漏损分析-高漏损报警")
void testAnalyzeHighAlarm() {
BigDecimal supply = new BigDecimal("1000");
BigDecimal sale = new BigDecimal("700");
when(leakageMapper.insert(any(DmaLeakageAnalysis.class))).thenReturn(1);
DmaLeakageAnalysis result = leakageService.analyze(1L, LocalDate.now(), supply, sale);
assertEquals("critical", result.getAlarmLevel());
}
@Test
@DisplayName("执行漏损分析-零供水")
void testAnalyzeZeroSupply() {
BigDecimal supply = BigDecimal.ZERO;
BigDecimal sale = BigDecimal.ZERO;
when(leakageMapper.insert(any(DmaLeakageAnalysis.class))).thenReturn(1);
DmaLeakageAnalysis result = leakageService.analyze(1L, LocalDate.now(), supply, sale);
assertEquals(0, BigDecimal.ZERO.compareTo(result.getNrwRate()));
assertEquals("normal", result.getAlarmLevel());
}
@Test
@DisplayName("获取漏损趋势")
void testGetTrend() {
DmaLeakageAnalysis analysis = new DmaLeakageAnalysis();
analysis.setZoneId(1L);
analysis.setAnalysisDate(LocalDate.now());
analysis.setNrwRate(new BigDecimal("12.5"));
when(leakageMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(analysis));
List<Map<String, Object>> trend = leakageService.getTrend(1L, 30);
assertEquals(1, trend.size());
assertEquals(new BigDecimal("12.5"), trend.get(0).get("nrwRate"));
}
@Test
@DisplayName("获取分区漏损汇总-无数据")
void testGetZoneSummaryEmpty() {
when(zoneMapper.selectById(1L)).thenReturn(null);
when(leakageMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of());
Map<String, Object> summary = leakageService.getZoneSummary(1L);
assertEquals(0, ((BigDecimal) summary.get("avgNrwRate")).compareTo(BigDecimal.ZERO));
assertEquals(0, summary.get("alarmCount"));
}
@Test
@DisplayName("获取报警列表")
void testGetAlarms() {
DmaLeakageAnalysis alarm = new DmaLeakageAnalysis();
alarm.setAlarmLevel("critical");
when(leakageMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(alarm));
List<DmaLeakageAnalysis> alarms = leakageService.getAlarms(null);
assertFalse(alarms.isEmpty());
}
}
@@ -0,0 +1,100 @@
package com.water.dma.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.dma.entity.DmaMeter;
import com.water.dma.mapper.DmaMeterMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* DMA计量表服务测试
*/
@ExtendWith(MockitoExtension.class)
class DmaMeterServiceTest {
@Mock
private DmaMeterMapper meterMapper;
private DmaMeterService meterService;
@BeforeEach
void setUp() {
meterService = new DmaMeterService(meterMapper);
}
@Test
@DisplayName("创建计量表")
void testCreateMeter() {
DmaMeter meter = new DmaMeter();
meter.setZoneId(1L);
meter.setMeterCode("M-001");
meter.setMeterName("进水表1");
meter.setMeterType("inlet");
when(meterMapper.insert(any(DmaMeter.class))).thenReturn(1);
DmaMeter created = meterService.create(meter);
assertNotNull(created);
assertEquals("online", created.getStatus());
verify(meterMapper).insert(any(DmaMeter.class));
}
@Test
@DisplayName("绑定计量表到分区")
void testBindToZone() {
DmaMeter meter = new DmaMeter();
meter.setId(1L);
meter.setMeterCode("M-001");
meter.setZoneId(null);
when(meterMapper.selectById(1L)).thenReturn(meter);
when(meterMapper.updateById(any(DmaMeter.class))).thenReturn(1);
meterService.bindToZone(1L, 2L);
assertEquals(2L, meter.getZoneId());
verify(meterMapper).updateById(any(DmaMeter.class));
}
@Test
@DisplayName("绑定不存在的计量表-失败")
void testBindToZoneNotFound() {
when(meterMapper.selectById(999L)).thenReturn(null);
assertThrows(RuntimeException.class, () -> {
meterService.bindToZone(999L, 1L);
});
}
@Test
@DisplayName("统计分区表计数量")
void testCountByZoneId() {
when(meterMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(5L);
Long count = meterService.countByZoneId(1L);
assertEquals(5L, count);
}
@Test
@DisplayName("获取分区下的所有计量表")
void testListByZoneId() {
DmaMeter meter = new DmaMeter();
meter.setId(1L);
meter.setZoneId(1L);
when(meterMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(meter));
List<DmaMeter> meters = meterService.listByZoneId(1L);
assertEquals(1, meters.size());
}
}
@@ -0,0 +1,112 @@
package com.water.dma.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.dma.entity.DmaZone;
import com.water.dma.mapper.DmaZoneMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* DMA分区服务测试
*/
@ExtendWith(MockitoExtension.class)
class DmaZoneServiceTest {
@Mock
private DmaZoneMapper zoneMapper;
private DmaZoneService zoneService;
@BeforeEach
void setUp() {
zoneService = new DmaZoneService(zoneMapper);
}
@Test
@DisplayName("创建DMA分区")
void testCreateZone() {
DmaZone zone = new DmaZone();
zone.setZoneName("测试分区A");
zone.setZoneCode("DMA-001");
zone.setZoneLevel(1);
zone.setArea("城北区");
when(zoneMapper.insert(any(DmaZone.class))).thenReturn(1);
DmaZone created = zoneService.create(zone);
assertNotNull(created);
assertEquals("active", created.getStatus());
verify(zoneMapper).insert(any(DmaZone.class));
}
@Test
@DisplayName("获取分区树形结构")
void testGetZoneTree() {
DmaZone parent = new DmaZone();
parent.setId(1L);
parent.setZoneName("总区");
parent.setZoneCode("ROOT");
parent.setZoneLevel(1);
parent.setParentId(null);
DmaZone child = new DmaZone();
child.setId(2L);
child.setZoneName("子区A");
child.setZoneCode("A");
child.setZoneLevel(2);
child.setParentId(1L);
when(zoneMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Arrays.asList(parent, child));
List<Map<String, Object>> tree = zoneService.getZoneTree();
assertNotNull(tree);
assertEquals(1, tree.size());
assertEquals("总区", tree.get(0).get("zoneName"));
}
@Test
@DisplayName("删除分区-存在子分区时失败")
void testDeleteZoneWithChildren() {
when(zoneMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
assertThrows(RuntimeException.class, () -> {
zoneService.delete(1L);
});
}
@Test
@DisplayName("删除分区-无子分区时成功")
void testDeleteZoneSuccess() {
when(zoneMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
when(zoneMapper.deleteById(1L)).thenReturn(1);
zoneService.delete(1L);
verify(zoneMapper).deleteById(1L);
}
@Test
@DisplayName("获取所有分区列表")
void testListAll() {
DmaZone zone = new DmaZone();
zone.setId(1L);
zone.setZoneName("测试分区");
when(zoneMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(zone));
List<DmaZone> zones = zoneService.listAll();
assertEquals(1, zones.size());
}
}
@@ -0,0 +1,117 @@
package com.water.dma.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.water.dma.entity.WaterBalance;
import com.water.dma.mapper.WaterBalanceMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 水平衡服务测试
*/
@ExtendWith(MockitoExtension.class)
class WaterBalanceServiceTest {
@Mock
private WaterBalanceMapper balanceMapper;
private WaterBalanceService balanceService;
@BeforeEach
void setUp() {
balanceService = new WaterBalanceService(balanceMapper);
}
@Test
@DisplayName("创建水平衡记录-自动计算漏损")
void testCreate() {
WaterBalance balance = new WaterBalance();
balance.setZoneId(1L);
balance.setPeriod("monthly");
balance.setStatDate(LocalDate.of(2024, 1, 1));
balance.setTotalSupply(new BigDecimal("10000"));
balance.setTotalSale(new BigDecimal("8500"));
when(balanceMapper.insert(any(WaterBalance.class))).thenReturn(1);
WaterBalance created = balanceService.create(balance);
assertNotNull(created);
assertEquals(0, new BigDecimal("1500").compareTo(created.getTotalLoss()));
assertEquals(0, new BigDecimal("15.00").compareTo(created.getNrwRate()));
verify(balanceMapper).insert(any(WaterBalance.class));
}
@Test
@DisplayName("创建水平衡-零供水")
void testCreateZeroSupply() {
WaterBalance balance = new WaterBalance();
balance.setZoneId(1L);
balance.setPeriod("daily");
balance.setStatDate(LocalDate.now());
balance.setTotalSupply(BigDecimal.ZERO);
balance.setTotalSale(BigDecimal.ZERO);
balance.setTotalLoss(BigDecimal.ZERO);
when(balanceMapper.insert(any(WaterBalance.class))).thenReturn(1);
WaterBalance created = balanceService.create(balance);
assertNotNull(created);
}
@Test
@DisplayName("生成水平衡报告-有数据")
void testGenerateReport() {
WaterBalance b1 = new WaterBalance();
b1.setTotalSupply(new BigDecimal("5000"));
b1.setTotalSale(new BigDecimal("4200"));
b1.setTotalLoss(new BigDecimal("800"));
b1.setApparentLoss(new BigDecimal("200"));
b1.setRealLoss(new BigDecimal("600"));
b1.setBillingSale(new BigDecimal("4000"));
b1.setFreeSupply(new BigDecimal("200"));
when(balanceMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(b1));
Map<String, Object> report = balanceService.generateReport(1L, "monthly",
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 31));
assertNotNull(report);
assertEquals(1, report.get("recordCount"));
assertEquals(new BigDecimal("5000"), report.get("totalSupply"));
assertEquals(new BigDecimal("800"), report.get("totalLoss"));
}
@Test
@DisplayName("生成水平衡报告-无数据")
void testGenerateReportEmpty() {
when(balanceMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of());
Map<String, Object> report = balanceService.generateReport(1L, "monthly",
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 31));
assertEquals(0, report.get("recordCount"));
assertEquals(BigDecimal.ZERO, report.get("totalSupply"));
}
@Test
@DisplayName("删除水平衡记录")
void testDelete() {
when(balanceMapper.deleteById(1L)).thenReturn(1);
balanceService.delete(1L);
verify(balanceMapper).deleteById(1L);
}
}