package cn.iocoder.yudao.module.mes.service.energy; import cn.iocoder.yudao.module.mes.dal.dataobject.energy.*; import cn.iocoder.yudao.module.mes.dal.mysql.energy.*; import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.service.user.AdminUserService; import com.baomidou.mybatisplus.core.conditions.Wrapper; import org.apache.ibatis.session.Configuration; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; import java.math.BigDecimal; import java.time.LocalDate; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class EnergyAlertServiceTest { @Mock EnergyAlertRuleMapper ruleMapper; @Mock EnergyAlertLogMapper logMapper; @Mock EnergyRecordMapper recordMapper; @Mock AdminUserService userService; @Mock NotifyMessageSendApi notifyApi; @InjectMocks EnergyAlertService service; final LocalDate date = LocalDate.of(2026, 10, 10); private EnergyAlertRuleDO rule() { var rule = new EnergyAlertRuleDO(); rule.setId(1L); rule.setEnabled(true); rule.setName("燕窝每日用水"); rule.setEnergyType("WATER"); rule.setMetric("QUANTITY"); rule.setUsageArea("燕窝生产区"); rule.setProductCategory("燕窝"); rule.setThreshold(new BigDecimal("100")); rule.setRecipientIds("7"); return rule; } private void prepare(EnergyAlertRuleDO rule, String actual) { when(ruleMapper.selectList(any(Wrapper.class))).thenReturn(List.of(rule)); when(ruleMapper.lock(1L)).thenReturn(rule); when(recordMapper.selectDailyValue(date, rule)).thenReturn(new BigDecimal(actual)); } private void user() { AdminUserDO user = new AdminUserDO(); user.setId(7L); user.setNickname("能耗管理员"); when(userService.getUser(7L)).thenReturn(user); } @Test void equalThresholdDoesNotNotify() { prepare(rule(), "100.000"); assertEquals(0, service.check(date).get("sent")); verifyNoInteractions(notifyApi, logMapper); assertFalse(EnergyAlertService.exceeds(BigDecimal.ZERO, BigDecimal.ONE)); } @Test void successCreatesMessageSnapshotAndRepeatedCheckDoesNotSendAgain() { prepare(rule(), "101"); user(); when(notifyApi.sendSingleMessageToAdmin(any())).thenReturn(33L); assertEquals(1, service.check(date).get("sent")); ArgumentCaptor history = ArgumentCaptor.forClass(EnergyAlertLogDO.class); verify(logMapper).insert(history.capture()); var stored = history.getValue(); stored.setId(9L); assertEquals("SENT", stored.getStatus()); assertEquals(33L, stored.getMessageId()); assertEquals(new BigDecimal("101"), stored.getActualValue()); ArgumentCaptor message = ArgumentCaptor.forClass(NotifySendSingleToUserReqDTO.class); verify(notifyApi).sendSingleMessageToAdmin(message.capture()); assertEquals("m³", message.getValue().getTemplateParams().get("unit")); assertEquals("2026-10-10", message.getValue().getTemplateParams().get("date")); when(logMapper.selectOne(any(Wrapper.class))).thenReturn(stored); assertEquals(0, service.check(date).get("sent")); verify(notifyApi, times(1)).sendSingleMessageToAdmin(any()); } @Test void disabledTemplateIsFailureAndLaterCheckRetriesSameHistory() { prepare(rule(), "101"); user(); when(notifyApi.sendSingleMessageToAdmin(any())).thenReturn(null).thenReturn(44L); assertEquals(1, service.check(date).get("failed")); ArgumentCaptor history = ArgumentCaptor.forClass(EnergyAlertLogDO.class); verify(logMapper).insert(history.capture()); var stored = history.getValue(); assertEquals("FAILED", stored.getStatus()); assertNull(stored.getMessageId()); stored.setId(9L); when(logMapper.selectOne(any(Wrapper.class))).thenReturn(stored); assertEquals(1, service.check(date).get("sent")); verify(logMapper).updateById(stored); assertEquals("SENT", stored.getStatus()); assertEquals("", stored.getErrorMessage()); } @Test void removedOrDisabledRuleIsSkippedAfterLock() { var rule = rule(); when(ruleMapper.selectList(any(Wrapper.class))).thenReturn(List.of(rule)); rule.setEnabled(false); when(ruleMapper.lock(1L)).thenReturn(rule); assertEquals(0, service.check(date).get("sent")); verifyNoInteractions(recordMapper, notifyApi); } @Test void oneFailedRecipientDoesNotPreventOthersAndMoneyHasYuanUnit() { var rule = rule(); rule.setRecipientIds("7,8"); rule.setMetric("AMOUNT"); prepare(rule, "150.55"); user(); AdminUserDO second = new AdminUserDO(); second.setNickname("主管"); when(userService.getUser(8L)).thenReturn(second); when(notifyApi.sendSingleMessageToAdmin(any())).thenThrow(new IllegalStateException("test failure")).thenReturn(77L); var result = service.check(date); assertEquals(1, result.get("sent")); assertEquals(1, result.get("failed")); ArgumentCaptor message = ArgumentCaptor.forClass(NotifySendSingleToUserReqDTO.class); verify(notifyApi, times(2)).sendSingleMessageToAdmin(message.capture()); assertEquals("元", message.getValue().getTemplateParams().get("unit")); verify(logMapper, times(2)).insert(any(EnergyAlertLogDO.class)); } @Test void dailySqlHasExactScopeFiltersAndBoundParameters() { Configuration configuration = new Configuration(); configuration.addMapper(EnergyRecordMapper.class); var rule = rule(); rule.setUsageArea("' OR 1=1 --"); rule.setMetric("AMOUNT"); String sql = configuration.getMappedStatement(EnergyRecordMapper.class.getName()+".selectDailyValue") .getBoundSql(Map.of("date", date, "rule", rule)).getSql(); assertTrue(sql.contains("SUM( amount")); assertTrue(sql.contains("usage_area=?")); assertTrue(sql.contains("deleted=0")); assertFalse(sql.contains(rule.getUsageArea())); } @Test void monthlyCostUsesFirstDayAndDeduplicatesDifferentDaysInSameMonth() { var rule = rule(); rule.setMetric("MONTH_AMOUNT"); when(ruleMapper.selectList(any(Wrapper.class))).thenReturn(List.of(rule)); when(ruleMapper.lock(1L)).thenReturn(rule); when(recordMapper.selectDailyValue(date.withDayOfMonth(1), rule)).thenReturn(new BigDecimal("150.55")); user(); when(notifyApi.sendSingleMessageToAdmin(any())).thenReturn(55L); assertEquals(1, service.check(date).get("sent")); var history = ArgumentCaptor.forClass(EnergyAlertLogDO.class); verify(logMapper).insert(history.capture()); var stored = history.getValue(); stored.setId(9L); assertEquals("MONTH", stored.getPeriodType()); assertEquals(date.withDayOfMonth(1), stored.getRecordDate()); var message = ArgumentCaptor.forClass(NotifySendSingleToUserReqDTO.class); verify(notifyApi).sendSingleMessageToAdmin(message.capture()); assertEquals("energy_monthly_warning", message.getValue().getTemplateCode()); assertEquals("2026-10", message.getValue().getTemplateParams().get("date")); assertEquals("元", message.getValue().getTemplateParams().get("unit")); when(logMapper.selectOne(any(Wrapper.class))).thenReturn(stored); assertEquals(0, service.check(date.plusDays(1)).get("sent")); verify(notifyApi, times(1)).sendSingleMessageToAdmin(any()); } @Test void monthlyQuantitySqlUsesWholeCalendarMonthAndCostMetricSelectsAmount() { var config = new Configuration(); config.addMapper(EnergyRecordMapper.class); var statement = config.getMappedStatement(EnergyRecordMapper.class.getName() + ".selectDailyValue"); var rule = rule(); rule.setMetric("MONTH_QUANTITY"); String sql = statement.getBoundSql(Map.of("date", LocalDate.of(2028,2,1), "rule", rule)).getSql(); assertTrue(sql.contains("SUM( quantity")); assertTrue(sql.contains("DATE_ADD(?, INTERVAL 1 MONTH)")); assertTrue(sql.contains("record_date >= ?")); assertTrue(sql.contains("record_date < DATE_ADD")); rule.setMetric("MONTH_AMOUNT"); assertTrue(statement.getBoundSql(Map.of("date", LocalDate.of(2026,12,1), "rule", rule)).getSql().contains("SUM( amount")); } }