feat(project): 实现临期逾期告警功能

- 新增告警记录表 rdms_due_alert_record 用于去重控制
- 添加告警相关常量类 DueAlertConstants 和对象类型枚举
- 在各数据访问层增加告警候选查询方法
- 实现告警候选服务类和站内信等级功能
- 添加临期逾期告警模板常量定义
- 扩展站内信发送接口支持消息等级
- 新增未读消息批量查询功能用于重复发送判定
This commit is contained in:
2026-06-13 15:00:36 +08:00
parent 635c18767e
commit 896ef0f127
41 changed files with 1650 additions and 18 deletions

View File

@@ -2,6 +2,8 @@ package com.njcn.rdms.module.system.api.notify;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.module.system.api.notify.dto.NotifySingleSendReqDTO;
import com.njcn.rdms.module.system.api.notify.dto.NotifyUnreadMessageListReqDTO;
import com.njcn.rdms.module.system.api.notify.dto.NotifyUnreadMessageRespDTO;
import com.njcn.rdms.module.system.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -10,6 +12,7 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
import java.util.Map;
/**
@@ -42,4 +45,26 @@ public interface NotifyMessageSendApi {
.setTemplateCode(templateCode).setTemplateParams(templateParams)).checkError();
}
/**
* 便捷方法:发送带等级的单条站内信给管理后台用户。
*
* @param level 消息等级,见 {@link com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants}
*/
default void sendSingleNotifyToAdmin(Long userId, String templateCode,
Map<String, Object> templateParams, Integer level) {
sendSingleNotify(new NotifySingleSendReqDTO().setUserId(userId)
.setTemplateCode(templateCode).setTemplateParams(templateParams).setLevel(level)).checkError();
}
/**
* 查询一批用户在指定模板下的未读站内信userType 固定 ADMIN由实现层处理
*
* <p>供业务方做"未读不重发"类判定(如临期/逾期告警);
* 与发送同挂在本 API复用既有 Feign 注册,消息域的跨模块入口保持唯一。</p>
*/
@PostMapping(PREFIX + "/unread-list-by-template-codes")
@Operation(summary = "查询一批用户在指定模板下的未读站内信")
CommonResult<List<NotifyUnreadMessageRespDTO>> getUnreadNotifyMessageListByTemplateCodes(
@Valid @RequestBody NotifyUnreadMessageListReqDTO reqDTO);
}

View File

@@ -1,5 +1,6 @@
package com.njcn.rdms.module.system.api.notify.dto;
import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -28,4 +29,7 @@ public class NotifySingleSendReqDTO {
@Schema(description = "模板参数(占位符 -> 值)", example = "{\"taskName\":\"联调\"}")
private Map<String, Object> templateParams;
@Schema(description = "消息等级1普通/2提醒/3警告/4严重默认普通", example = "1")
private Integer level = NotifyMessageLevelConstants.NORMAL;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.rdms.module.system.api.notify.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* 查询一批用户在指定模板下的未读站内信 Request DTO。
*
* <p>业务方按模板维度查未读(如临期/逾期告警的"未读不重发"判定),
* userType 在能力层固定 ADMIN不暴露给业务方与发送口径一致。</p>
*
* @author hongawen
*/
@Schema(description = "RPC 服务 - 查询未读站内信 Request DTO")
@Data
@Accessors(chain = true)
public class NotifyUnreadMessageListReqDTO {
@Schema(description = "接收用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024,2048]")
@NotEmpty(message = "用户编号列表不能为空")
private List<Long> userIds;
@Schema(description = "站内信模板编码列表", requiredMode = Schema.RequiredMode.REQUIRED,
example = "[\"due_alert_overdue_owner\"]")
@NotEmpty(message = "模板编码列表不能为空")
private List<String> templateCodes;
}

View File

@@ -0,0 +1,30 @@
package com.njcn.rdms.module.system.api.notify.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Map;
/**
* 未读站内信 Response DTO按模板查询的精简视图
*
* <p>只回传业务判定需要的三个字段;正文等展示字段不在跨模块契约里暴露。</p>
*
* @author hongawen
*/
@Schema(description = "RPC 服务 - 未读站内信 Response DTO")
@Data
@Accessors(chain = true)
public class NotifyUnreadMessageRespDTO {
@Schema(description = "接收用户编号", example = "1024")
private Long userId;
@Schema(description = "模板编码", example = "due_alert_overdue_owner")
private String templateCode;
@Schema(description = "发送时的模板参数(含业务附加参数,如 objectType/objectId")
private Map<String, Object> templateParams;
}

View File

@@ -0,0 +1,26 @@
package com.njcn.rdms.module.system.enums.notify;
/**
* 站内信消息等级常量(值 1-4数字越大越严重
*
* <p>等级「值」供代码逻辑引用(如告警按场景定级);等级「展示」(名字/颜色) 走字典
* {@link #DICT_TYPE},运维可调、不发版。本类不做合法集合校验、不做枚举全集
* 沿用本仓库「DB 动态配置字段不做代码侧校验」习惯)。</p>
*/
public final class NotifyMessageLevelConstants {
private NotifyMessageLevelConstants() {
}
/** 普通(默认,灰) */
public static final int NORMAL = 1;
/** 提醒(黄) */
public static final int REMIND = 2;
/** 警告(橙) */
public static final int WARN = 3;
/** 严重(红) */
public static final int SEVERE = 4;
/** 等级展示字典类型(前端拉取渲染徽标) */
public static final String DICT_TYPE = "notify_message_level";
}

View File

@@ -1,17 +1,26 @@
package com.njcn.rdms.module.system.api.notify;
import com.njcn.rdms.framework.common.enums.UserTypeEnum;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.api.notify.dto.NotifySingleSendReqDTO;
import com.njcn.rdms.module.system.api.notify.dto.NotifyUnreadMessageListReqDTO;
import com.njcn.rdms.module.system.api.notify.dto.NotifyUnreadMessageRespDTO;
import com.njcn.rdms.module.system.dal.dataobject.notify.NotifyMessageDO;
import com.njcn.rdms.module.system.service.notify.NotifyMessageService;
import com.njcn.rdms.module.system.service.notify.NotifySendService;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
/**
* {@link NotifyMessageSendApi} 的实现:委托现有 {@link NotifySendService},不重写发送逻辑。
* 未读查询同理委托 {@link NotifyMessageService}userType 固定 ADMIN与发送口径一致
*
* @author hongawen
*/
@@ -22,12 +31,22 @@ public class NotifyMessageSendApiImpl implements NotifyMessageSendApi {
@Resource
private NotifySendService notifySendService;
@Resource
private NotifyMessageService notifyMessageService;
@Override
public CommonResult<Long> sendSingleNotify(NotifySingleSendReqDTO reqDTO) {
Long logId = notifySendService.sendSingleNotifyToAdmin(
reqDTO.getUserId(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams());
Long logId = notifySendService.sendSingleNotify(reqDTO.getUserId(), UserTypeEnum.ADMIN.getValue(),
reqDTO.getTemplateCode(), reqDTO.getTemplateParams(), reqDTO.getLevel());
return success(logId);
}
@Override
public CommonResult<List<NotifyUnreadMessageRespDTO>> getUnreadNotifyMessageListByTemplateCodes(
NotifyUnreadMessageListReqDTO reqDTO) {
List<NotifyMessageDO> list = notifyMessageService.getUnreadNotifyMessageListByTemplateCodes(
reqDTO.getUserIds(), UserTypeEnum.ADMIN.getValue(), reqDTO.getTemplateCodes());
return success(BeanUtils.toBean(list, NotifyUnreadMessageRespDTO.class));
}
}

View File

@@ -43,6 +43,9 @@ public class NotifyMessageRespVO {
@Schema(description = "阅读时间")
private LocalDateTime readTime;
@Schema(description = "消息等级1普通/2提醒/3警告/4严重", example = "1")
private Integer level;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;

View File

@@ -93,5 +93,11 @@ public class NotifyMessageDO extends BaseDO {
* 阅读时间
*/
private LocalDateTime readTime;
/**
* 消息等级1普通/2提醒/3警告/4严重默认普通
*
* 值见 {@link com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants}
*/
private Integer level;
}

View File

@@ -70,4 +70,15 @@ public interface NotifyMessageMapper extends BaseMapperX<NotifyMessageDO> {
.eq(NotifyMessageDO::getUserType, userType));
}
/** 一批用户在指定模板下的未读消息调用方保证两个集合非空IN 空集会报错) */
default List<NotifyMessageDO> selectUnreadListByUserIdsAndTemplateCodes(Collection<Long> userIds,
Integer userType,
Collection<String> templateCodes) {
return selectList(new LambdaQueryWrapperX<NotifyMessageDO>()
.eq(NotifyMessageDO::getReadStatus, false)
.eq(NotifyMessageDO::getUserType, userType)
.in(NotifyMessageDO::getUserId, userIds)
.in(NotifyMessageDO::getTemplateCode, templateCodes));
}
}

View File

@@ -25,10 +25,12 @@ public interface NotifyMessageService {
* @param template 模版信息
* @param templateContent 模版内容
* @param templateParams 模版参数
* @param level 消息等级null 视为普通),见 NotifyMessageLevelConstants
* @return 站内信编号
*/
Long createNotifyMessage(Long userId, Integer userType,
NotifyTemplateDO template, String templateContent, Map<String, Object> templateParams);
NotifyTemplateDO template, String templateContent,
Map<String, Object> templateParams, Integer level);
/**
* 获得站内信分页
@@ -75,6 +77,17 @@ public interface NotifyMessageService {
*/
Long getUnreadNotifyMessageCount(Long userId, Integer userType);
/**
* 查询一批用户在指定模板下的未读站内信(业务方"未读不重发"判定用)
*
* @param userIds 用户编号列表
* @param userType 用户类型
* @param templateCodes 模板编码列表
* @return 未读站内信列表(任一入参为空集时返回空列表)
*/
List<NotifyMessageDO> getUnreadNotifyMessageListByTemplateCodes(Collection<Long> userIds, Integer userType,
Collection<String> templateCodes);
/**
* 标记站内信为已读
*

View File

@@ -6,6 +6,7 @@ import com.njcn.rdms.module.system.controller.admin.notify.vo.message.NotifyMess
import com.njcn.rdms.module.system.dal.dataobject.notify.NotifyMessageDO;
import com.njcn.rdms.module.system.dal.dataobject.notify.NotifyTemplateDO;
import com.njcn.rdms.module.system.dal.mysql.notify.NotifyMessageMapper;
import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -28,11 +29,13 @@ public class NotifyMessageServiceImpl implements NotifyMessageService {
@Override
public Long createNotifyMessage(Long userId, Integer userType,
NotifyTemplateDO template, String templateContent, Map<String, Object> templateParams) {
NotifyTemplateDO template, String templateContent,
Map<String, Object> templateParams, Integer level) {
NotifyMessageDO message = new NotifyMessageDO().setUserId(userId).setUserType(userType)
.setTemplateId(template.getId()).setTemplateCode(template.getCode())
.setTemplateType(template.getType()).setTemplateNickname(template.getNickname())
.setTemplateContent(templateContent).setTemplateParams(templateParams).setReadStatus(false);
.setTemplateContent(templateContent).setTemplateParams(templateParams).setReadStatus(false)
.setLevel(level == null ? NotifyMessageLevelConstants.NORMAL : level);
notifyMessageMapper.insert(message);
return message.getId();
}
@@ -62,6 +65,16 @@ public class NotifyMessageServiceImpl implements NotifyMessageService {
return notifyMessageMapper.selectUnreadCountByUserIdAndUserType(userId, userType);
}
@Override
public List<NotifyMessageDO> getUnreadNotifyMessageListByTemplateCodes(Collection<Long> userIds, Integer userType,
Collection<String> templateCodes) {
// 空集守卫IN 空集合会生成非法 SQL
if (userIds == null || userIds.isEmpty() || templateCodes == null || templateCodes.isEmpty()) {
return List.of();
}
return notifyMessageMapper.selectUnreadListByUserIdsAndTemplateCodes(userIds, userType, templateCodes);
}
@Override
public int updateNotifyMessageRead(Collection<Long> ids, Long userId, Integer userType) {
return notifyMessageMapper.updateListRead(ids, userId, userType);

View File

@@ -1,5 +1,7 @@
package com.njcn.rdms.module.system.service.notify;
import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants;
import java.util.List;
import java.util.Map;
@@ -36,7 +38,7 @@ public interface NotifySendService {
String templateCode, Map<String, Object> templateParams);
/**
* 发送单条站内信给用户
* 发送单条站内信给用户(默认普通等级)
*
* @param userId 用户编号
* @param userType 用户类型
@@ -44,8 +46,24 @@ public interface NotifySendService {
* @param templateParams 站内信模板参数
* @return 发送日志编号
*/
Long sendSingleNotify( Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams);
default Long sendSingleNotify(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams) {
return sendSingleNotify(userId, userType, templateCode, templateParams,
NotifyMessageLevelConstants.NORMAL);
}
/**
* 发送单条带等级的站内信给用户
*
* @param userId 用户编号
* @param userType 用户类型
* @param templateCode 站内信模板编号
* @param templateParams 站内信模板参数
* @param level 消息等级,可为 null由落库层兜底为普通见 NotifyMessageLevelConstants
* @return 发送日志编号
*/
Long sendSingleNotify(Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams, Integer level);
default void sendBatchNotify(List<String> mobiles, List<Long> userIds, Integer userType,
String templateCode, Map<String, Object> templateParams) {

View File

@@ -43,7 +43,8 @@ public class NotifySendServiceImpl implements NotifySendService {
}
@Override
public Long sendSingleNotify(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams) {
public Long sendSingleNotify(Long userId, Integer userType, String templateCode,
Map<String, Object> templateParams, Integer level) {
// 校验模版
NotifyTemplateDO template = validateNotifyTemplate(templateCode);
if (Objects.equals(template.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
@@ -55,7 +56,7 @@ public class NotifySendServiceImpl implements NotifySendService {
// 发送站内信
String content = notifyTemplateService.formatNotifyTemplateContent(template.getContent(), templateParams);
return notifyMessageService.createNotifyMessage(userId, userType, template, content, templateParams);
return notifyMessageService.createNotifyMessage(userId, userType, template, content, templateParams, level);
}
@VisibleForTesting

View File

@@ -0,0 +1,10 @@
-- 站内信消息等级:通用消息表加 level 列1普通/2提醒/3警告/4严重默认普通
-- 设计见 docs/superpowers/specs/2026-06-13-告警消息等级-design.md
-- 幂等MySQL 不支持 ADD COLUMN IF NOT EXISTS用 information_schema 判断后 PREPARE 执行
-- 列定义/注释须与演示库补丁 docs/sql/patches/2026-06-13-告警消息等级-01.sql 块1 保持一致
SET @col_exists = (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'system_notify_message' AND column_name = 'level');
SET @ddl = IF(@col_exists = 0,
'ALTER TABLE system_notify_message ADD COLUMN level TINYINT NOT NULL DEFAULT 1 COMMENT ''消息等级:1普通/2提醒/3警告/4严重'' AFTER read_time',
'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,54 @@
package com.njcn.rdms.module.system.service.notify;
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
import com.njcn.rdms.module.system.dal.dataobject.notify.NotifyMessageDO;
import com.njcn.rdms.module.system.dal.dataobject.notify.NotifyTemplateDO;
import com.njcn.rdms.module.system.dal.mysql.notify.NotifyMessageMapper;
import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
/**
* 站内信落库单测:消息等级 level 正确落库(指定值 + null 兜底普通)。
*/
class NotifyMessageServiceImplTest extends BaseMockitoUnitTest {
@InjectMocks
private NotifyMessageServiceImpl notifyMessageService;
@Mock
private NotifyMessageMapper notifyMessageMapper;
private NotifyTemplateDO template() {
NotifyTemplateDO t = new NotifyTemplateDO();
t.setId(1L);
t.setCode("due_alert_overdue_owner");
t.setType(3);
t.setNickname("系统通知");
return t;
}
@Test
void createNotifyMessage_shouldPersistGivenLevel() {
notifyMessageService.createNotifyMessage(100L, 2, template(), "正文", null,
NotifyMessageLevelConstants.SEVERE);
ArgumentCaptor<NotifyMessageDO> captor = ArgumentCaptor.forClass(NotifyMessageDO.class);
verify(notifyMessageMapper).insert(captor.capture());
assertEquals(NotifyMessageLevelConstants.SEVERE, captor.getValue().getLevel());
}
@Test
void createNotifyMessage_nullLevel_shouldFallbackToNormal() {
notifyMessageService.createNotifyMessage(100L, 2, template(), "正文", null, null);
ArgumentCaptor<NotifyMessageDO> captor = ArgumentCaptor.forClass(NotifyMessageDO.class);
verify(notifyMessageMapper).insert(captor.capture());
assertEquals(NotifyMessageLevelConstants.NORMAL, captor.getValue().getLevel());
}
}