feat(bpm): 待办任务站内信实时推送
- BPM 任务分配时生成站内信并通过 WebSocket 实时推送,铃铛无需点击即可显示未读
- 修复 WebSocket 握手 userType 为空导致会话被断开的问题
- 新增待办通知消息模板配置 SQL
Co-Authored-By: Claude Code <noreply@anthropic.com>
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ============================================================================= |
| | | -- BPM å¾
åä»»å¡ç«å
ä¿¡éç¥æ¨¡æ¿ |
| | | -- ç®æ åºï¼mom-ntsh |
| | | -- èæ¯ï¼BpmMessageServiceImpl#sendMessageWhenTaskAssigned å·²æ¹ä¸ºåéç«å
ä¿¡ï¼ |
| | | -- ä¾èµ system_notify_template ä¸åå¨ code=bpm_task_assigned çæ¨¡æ¿ï¼ |
| | | -- 缺失æ¶ä¼æãç«å
信模æ¿ä¸åå¨ãå¹¶è·³è¿åéï¼å·²æè·å¼å¸¸ï¼ä¸å½±åæµç¨æäº¤ï¼ã |
| | | -- åæ°ï¼processNameï¼æµç¨åç§°ï¼ãtaskNameï¼ä»»å¡èç¹ï¼ãstartUserNameï¼æäº¤äººï¼ |
| | | -- å¹çï¼code å·²åå¨åè·³è¿ï¼å¯é夿§è¡ã |
| | | -- ============================================================================= |
| | | |
| | | SET NAMES utf8mb4; |
| | | |
| | | INSERT INTO system_notify_template |
| | | (name, code, nickname, content, type, params, status, remark, creator, updater) |
| | | SELECT 'æµç¨å¾
å任塿é', 'bpm_task_assigned', '审æ¹ä¸å¿', |
| | | 'æ¨ææ°çå¾
åä»»å¡ï¼{processName} -ã{taskName}ãï¼æäº¤äººï¼{startUserName}ã请å°ã审æ¹ä¸å¿ â å¾
åä»»å¡ãåæ¶å¤çã', |
| | | 1, '["processName","taskName","startUserName"]', 0, |
| | | 'BPM ä»»å¡åé
å°å®¡æ¹äººæ¶åéï¼ç¹å»éç¥ç´æ¥è·³è½¬å¾
åä»»å¡', '1', '1' |
| | | WHERE NOT EXISTS (SELECT 1 FROM system_notify_template |
| | | WHERE code = 'bpm_task_assigned' AND deleted = b'0'); |
| | | |
| | | -- æ ¡éª |
| | | SELECT id, name, code, nickname, content, params, status |
| | | FROM system_notify_template |
| | | WHERE code = 'bpm_task_assigned' AND deleted = b'0'; |
| | |
| | | package cn.iocoder.yudao.framework.websocket.core.security; |
| | | |
| | | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| | | import cn.iocoder.yudao.framework.security.core.LoginUser; |
| | | import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; |
| | | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| | |
| | | if (loginUser == null) { |
| | | return false; |
| | | } |
| | | // WebSocket å°åï¼å¦ /infra/wsï¼ä¸å¸¦ /admin-apiã/app-api åç¼ï¼ |
| | | // å æ¤ mock ç»å½æ¶ WebFrameworkUtils#getLoginUserType åä¸å°ç¨æ·ç±»åã |
| | | // è¿éå
åºä¸ºç®¡çåç±»åï¼é¿å
WebSocketSessionManagerImpl ç¨ null ä½ä¸º key è®°å½ä¼è¯æ¶æ NPE æå¼è¿æ¥ã |
| | | if (loginUser.getUserType() == null) { |
| | | loginUser.setUserType(UserTypeEnum.ADMIN.getValue()); |
| | | } |
| | | WebSocketFrameworkUtils.setLoginUser(loginUser, attributes); |
| | | return true; |
| | | } |
| | |
| | | idSessions.put(session.getId(), session); |
| | | // æ·»å å° userSessions ä¸ |
| | | LoginUser user = WebSocketFrameworkUtils.getLoginUser(session); |
| | | if (user == null) { |
| | | // ç¨æ·æç¨æ·ç±»å为空æ¶ä¸å¤çï¼ConcurrentHashMap ä¸å
许 null keyï¼ä¼ç´æ¥æ NPE æå¼è¿æ¥ |
| | | if (user == null || user.getUserType() == null || user.getId() == null) { |
| | | return; |
| | | } |
| | | ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType()); |
| | |
| | | idSessions.remove(session.getId()); |
| | | // ç§»é¤ä» idSessions ä¸ |
| | | LoginUser user = WebSocketFrameworkUtils.getLoginUser(session); |
| | | if (user == null) { |
| | | // ç¨æ·æç¨æ·ç±»å为空æ¶ä¸å¤çï¼ConcurrentHashMap ä¸å
许 null keyï¼ä¼ç´æ¥æ NPE æå¼è¿æ¥ |
| | | if (user == null || user.getUserType() == null || user.getId() == null) { |
| | | return; |
| | | } |
| | | ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType()); |
| | |
| | | package cn.iocoder.yudao.module.bpm.service.message; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; |
| | | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; |
| | | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; |
| | | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO; |
| | | import cn.iocoder.yudao.module.system.service.notify.NotifySendService; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.flowable.engine.TaskService; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * BPM æ¶æ¯ Service å®ç°ç±» |
| | | * |
| | | * 注æï¼å·²å
³éçä¿¡éç¥åè½ï¼ç¸å
³æ¹æ³ä»
è®°å½æ¥å¿ï¼ä¸åè°ç¨çä¿¡å鿥å£ï¼ |
| | | * 以é¿å
âç信模æ¿ä¸åå¨âçé®é¢ã |
| | | * å½ååªä¿çãå¾
åä»»å¡ãçç«å
ä¿¡éç¥ï¼ä»»å¡åé
å°å
·ä½å®¡æ¹äººæ¶ï¼ç»å®¡æ¹äººå䏿¡ç«å
ä¿¡ï¼ç³»ç»å³ä¸è§å°ééå¯è§ï¼ã |
| | | * çä¿¡éç¥å·²æ´ä½å
³éï¼å
¶ä½æ¹æ³ä»
è®°å½æ¥å¿ã |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | |
| | | @Validated |
| | | @Slf4j |
| | | public class BpmMessageServiceImpl implements BpmMessageService { |
| | | |
| | | /** |
| | | * å¾
åä»»å¡ç«å
信模æ¿ç¼ç ï¼å¯¹åº system_notify_template.code |
| | | */ |
| | | private static final String TEMPLATE_CODE_TASK_ASSIGNED = "bpm_task_assigned"; |
| | | |
| | | /** |
| | | * ç«å
ä¿¡åæ°å¼çæå¤§é¿åº¦ã |
| | | * 说æï¼system_notify_message.template_params åªæ varchar(255)ï¼åæ°å¼è¿é¿ä¼å¯¼è´æå
¥å¤±è´¥ï¼ |
| | | * è¿éç»ä¸æªæï¼ä¿è¯ä¸æ¡å¾
åå¿
å®è½ååºå»ã |
| | | */ |
| | | private static final int PARAM_VALUE_MAX_LENGTH = 60; |
| | | |
| | | @Resource |
| | | private NotifySendService notifySendService; |
| | | |
| | | @Resource |
| | | private TaskService flowTaskService; |
| | | |
| | | @Override |
| | | public void sendMessageWhenProcessInstanceApprove(BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO) { |
| | |
| | | log.info("[sendMessageWhenProcessInstanceReject][å·²å
³éçä¿¡éç¥ï¼è·³è¿åé][processInstanceId: {}]", reqDTO.getProcessInstanceId()); |
| | | } |
| | | |
| | | /** |
| | | * ä»»å¡åé
å°å®¡æ¹äººæ¶ï¼åéç«å
ä¿¡éç¥ã |
| | | * |
| | | * 说æï¼æææµç¨èç¹çå¾
åä»»å¡é½ä¼èµ° BpmTaskServiceImpl#processTaskAssigned çæ¬æ¬¡è°ç¨ï¼å ç¾ã转ååæ ·ä¼è§¦åï¼ï¼ |
| | | * å æ¤è¿éå®ç°ä¸æ¬¡å³å¯è¦çå
¨é¨æµç¨çå¾
åéç¥ã |
| | | */ |
| | | @Override |
| | | public void sendMessageWhenTaskAssigned(BpmMessageSendWhenTaskCreatedReqDTO reqDTO) { |
| | | log.info("[sendMessageWhenTaskAssigned][å·²å
³éçä¿¡éç¥ï¼è·³è¿åé][processInstanceId: {}]", reqDTO.getProcessInstanceId()); |
| | | if (reqDTO.getAssigneeUserId() == null) { |
| | | return; |
| | | } |
| | | // ä»»å¡å¯è½å¨åä¸äºå¡é已被èªå¨éè¿ãèªå¨æç»æåæ¶ï¼å
审æ¹å¼å
³ãæç¾è¢«æ¢å çï¼ï¼æ¤æ¶åå¾
åéç¥åªä¼è¯¯å¯¼å®¡æ¹äºº |
| | | if (flowTaskService.createTaskQuery().taskId(reqDTO.getTaskId()).count() == 0) { |
| | | log.info("[sendMessageWhenTaskAssigned][taskId({}) å·²ä¸å¨å¾
åä¸ï¼è·³è¿å¾
åéç¥]", reqDTO.getTaskId()); |
| | | return; |
| | | } |
| | | Map<String, Object> templateParams = new HashMap<>(4); |
| | | templateParams.put("processName", StrUtil.maxLength(reqDTO.getProcessInstanceName(), PARAM_VALUE_MAX_LENGTH)); |
| | | templateParams.put("taskName", StrUtil.maxLength(reqDTO.getTaskName(), PARAM_VALUE_MAX_LENGTH)); |
| | | templateParams.put("startUserName", StrUtil.maxLength(reqDTO.getStartUserNickname(), PARAM_VALUE_MAX_LENGTH)); |
| | | try { |
| | | notifySendService.sendSingleNotifyToAdmin(reqDTO.getAssigneeUserId(), TEMPLATE_CODE_TASK_ASSIGNED, templateParams); |
| | | log.info("[sendMessageWhenTaskAssigned][å·²åç¨æ·({})åéå¾
åç«å
ä¿¡][taskId({}) taskName({})]", |
| | | reqDTO.getAssigneeUserId(), reqDTO.getTaskId(), reqDTO.getTaskName()); |
| | | } catch (Exception ex) { |
| | | // ç«å
ä¿¡åé失败ä¸è½å½±å审æ¹ä»»å¡æ¬èº«çå建ï¼å¦åæ´ä¸ªæµç¨æäº¤äºå¡ä¼è¢«åæ» |
| | | log.error("[sendMessageWhenTaskAssigned][åç¨æ·({})åéå¾
åç«å
信失败][taskId({})]", |
| | | reqDTO.getAssigneeUserId(), reqDTO.getTaskId(), ex); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| | | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| | | import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi; |
| | | import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyTemplateDO; |
| | | import cn.iocoder.yudao.module.system.service.notify.dto.NotifyMessagePushDTO; |
| | | import com.google.common.annotations.VisibleForTesting; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | |
| | | @Resource |
| | | private NotifyMessageService notifyMessageService; |
| | | |
| | | @Resource |
| | | private WebSocketSenderApi webSocketSenderApi; |
| | | |
| | | @Override |
| | | public Long sendSingleNotifyToAdmin(Long userId, String templateCode, Map<String, Object> templateParams) { |
| | | return sendSingleNotify(userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); |
| | |
| | | |
| | | // åéç«å
ä¿¡ |
| | | String content = notifyTemplateService.formatNotifyTemplateContent(template.getContent(), templateParams); |
| | | return notifyMessageService.createNotifyMessage(userId, userType, template, content, templateParams); |
| | | Long messageId = notifyMessageService.createNotifyMessage(userId, userType, template, content, templateParams); |
| | | // 宿¶æ¨éï¼å端ééæ¶å°åç«å³å·æ°æªè¯»æ°ï¼é¿å
åªè½çå¾
轮询 |
| | | pushToWebSocket(userId, userType, messageId, template, content); |
| | | return messageId; |
| | | } |
| | | |
| | | /** |
| | | * éè¿ WebSocket æç«å
ä¿¡å®æ¶æ¨éç»ç¨æ·ï¼è¯¥ç¨æ·ææå¨çº¿é¡µé¢é½ä¼æ¶å°ï¼ |
| | | * |
| | | * 说æï¼æ¨éåªæ¯âæåéç¥åç«¯å·æ°âï¼æ¨é失败ä¸å½±åç«å
ä¿¡æ¬èº«ï¼å æ¤è¿éåæå¼å¸¸ä»
è®°å½æ¥å¿ã |
| | | */ |
| | | private void pushToWebSocket(Long userId, Integer userType, Long messageId, |
| | | NotifyTemplateDO template, String content) { |
| | | if (messageId == null) { |
| | | return; |
| | | } |
| | | NotifyMessagePushDTO message = new NotifyMessagePushDTO(); |
| | | message.setMessageId(messageId); |
| | | message.setTemplateCode(template.getCode()); |
| | | message.setTemplateNickname(template.getNickname()); |
| | | message.setTemplateContent(content); |
| | | try { |
| | | webSocketSenderApi.sendObject(userType, userId, NotifyMessagePushDTO.MESSAGE_TYPE, message); |
| | | } catch (Exception ex) { |
| | | log.warn("[pushToWebSocket][åç¨æ·({}/{})æ¨éç«å
信({})失败]", userId, userType, messageId, ex); |
| | | } |
| | | } |
| | | |
| | | @VisibleForTesting |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.system.service.notify.dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * ç«å
ä¿¡ WebSocket 宿¶æ¨éçæ¶æ¯ä½ |
| | | * |
| | | * å端æ¶å° type = notify-message çæ¶æ¯åï¼å·æ°æªè¯»æ°éåæªè¯»åè¡¨ï¼æ éçå¾
轮询ã |
| | | * |
| | | * @author è¶
级管çå |
| | | */ |
| | | @Data |
| | | public class NotifyMessagePushDTO { |
| | | |
| | | /** |
| | | * æ¶æ¯ç±»åï¼ç«å
ä¿¡ |
| | | */ |
| | | public static final String MESSAGE_TYPE = "notify-message"; |
| | | |
| | | /** |
| | | * ç«å
ä¿¡ç¼å· |
| | | */ |
| | | private Long messageId; |
| | | |
| | | /** |
| | | * ç«å
信模æ¿ç¼ç |
| | | */ |
| | | private String templateCode; |
| | | |
| | | /** |
| | | * ç«å
ä¿¡åé人åç§° |
| | | */ |
| | | private String templateNickname; |
| | | |
| | | /** |
| | | * ç«å
ä¿¡å
容ï¼å·²æ ¼å¼åï¼ |
| | | */ |
| | | private String templateContent; |
| | | |
| | | } |