Forráskód Böngészése

feat: 企微 ID 设置和超时企微通知

wuwenyi 1 hete
szülő
commit
89c2506a85

+ 8 - 2
src/main/java/com/qqflow/engine/domain/ewechat/api/EnterpriseWechatApi.java

@@ -133,12 +133,18 @@ public class EnterpriseWechatApi {
         return this.commonPost(uri, req, EnterpriseWechatSendMsgRsp.class);
     }
 
-    /** 发送消息 */
-    public EnterpriseWechatSendMsgRsp sendMsg(String accessToken, EnterpriseWechatSendMsgReq req){
+    /** 发送客服消息 */
+    public EnterpriseWechatSendMsgRsp sendKfMsg(String accessToken, EnterpriseWechatSendMsgReq req){
         String uri = "/cgi-bin/kf/send_msg?access_token=" + accessToken;
         return this.commonPost(uri, req, EnterpriseWechatSendMsgRsp.class);
     }
 
+    /** 发送应用文本消息 */
+    public EnterpriseWechatSendTextMsgRsp sendTextMsg(String accessToken, EnterpriseWechatSendTextMsgReq req) {
+        String uri = "/cgi-bin/message/send?access_token=" + accessToken;
+        return this.commonPost(uri, req, EnterpriseWechatSendTextMsgRsp.class);
+    }
+
     /** 上传素材文件 */
     public EnterpriseWechatUploadRsp upload(String accessToken, String type, File file) {
         return this.upload(accessToken, type, getResource(file.getPath()), (int) file.length());

+ 3 - 0
src/main/java/com/qqflow/engine/domain/ewechat/config/EnterpriseWechatProperties.java

@@ -27,4 +27,7 @@ public class EnterpriseWechatProperties {
 
     /** 代开发应用模板secret */
     private String suiteSecret;
+
+    /** 授权方应用id */
+    private Integer agentId;
 }

+ 1 - 1
src/main/java/com/qqflow/engine/domain/ewechat/controller/EnterpriseTestController.java

@@ -68,7 +68,7 @@ public class EnterpriseTestController {
     @PostMapping("/send-img-msg")
     public Result<EnterpriseWechatSendMsgRsp> sendImg(@RequestBody EnterpriseWechatSendMsgReq req, EnterpriseCorpDto dto) {
         String accessToken = enterpriseWechatService.getAccessToken(dto.getCorpId(), dto.getSuiteId());
-        EnterpriseWechatSendMsgRsp sendMsgRsp = enterpriseWechatApi.sendMsg(accessToken, req);
+        EnterpriseWechatSendMsgRsp sendMsgRsp = enterpriseWechatApi.sendKfMsg(accessToken, req);
         return Result.ok(sendMsgRsp);
     }
 

+ 79 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSendTextMsgReq.java

@@ -0,0 +1,79 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+public class EnterpriseWechatSendTextMsgReq {
+
+    /** 指定接收消息的成员,多个用'|'分隔,最多1000个。@all表示全部成员 */
+    private String touser;
+
+    /** 指定接收消息的部门,多个用'|'分隔,最多100个 */
+    private String toparty;
+
+    /** 指定接收消息的标签,多个用'|'分隔,最多100个 */
+    private String totag;
+
+    /** 消息类型,固定为text */
+    private String msgtype = "text";
+
+    /** 企业应用的id */
+    private Integer agentid;
+
+    /** 文本内容 */
+    @JsonProperty("text")
+    private TextContent text;
+
+    /** 是否保密消息,0-可分享 1-水印,默认0 */
+    private Integer safe = 0;
+
+    @JsonProperty("enable_id_trans")
+    private Integer enableIdTrans = 0;
+
+    @JsonProperty("enable_duplicate_check")
+    private Integer enableDuplicateCheck = 0;
+
+    @JsonProperty("duplicate_check_interval")
+    private Integer duplicateCheckInterval = 1800;
+
+    @Data
+    public static class TextContent {
+        private String content;
+
+        public static TextContent of(String content) {
+            TextContent tc = new TextContent();
+            tc.setContent(content);
+            return tc;
+        }
+    }
+
+    public static EnterpriseWechatSendTextMsgReq toUser(String userId, Integer agentId, String content) {
+        EnterpriseWechatSendTextMsgReq req = new EnterpriseWechatSendTextMsgReq();
+        req.setTouser(userId);
+        req.setAgentid(agentId);
+        req.setText(TextContent.of(content));
+        return req;
+    }
+
+    /** 创建发送给指定用户的消息 */
+    public static EnterpriseWechatSendTextMsgReq toUsers(List<String> userIds, Integer agentId, String content) {
+        EnterpriseWechatSendTextMsgReq req = new EnterpriseWechatSendTextMsgReq();
+        req.setTouser(userIds.stream().collect(Collectors.joining("|")));
+        req.setAgentid(agentId);
+        req.setText(TextContent.of(content));
+        return req;
+    }
+
+    /** 创建发送给全部成员的消息 */
+    public static EnterpriseWechatSendTextMsgReq toAll(Integer agentId, String content) {
+        EnterpriseWechatSendTextMsgReq req = new EnterpriseWechatSendTextMsgReq();
+        req.setTouser("@all");
+        req.setAgentid(agentId);
+        req.setText(TextContent.of(content));
+        return req;
+    }
+}

+ 22 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatSendTextMsgRsp.java

@@ -0,0 +1,22 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Data;
+
+@Data
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatSendTextMsgRsp extends EnterpriseWechatBaseRsp {
+    /** 不合法的userid,不区分大小写,统一转为小写 */
+    private String invaliduser;
+    /** 不合法的partyid */
+    private String invalidparty;
+    /** 不合法的标签id */
+    private String invalidtag;
+    /** 没有基础接口许可(包含已过期)的userid */
+    private String unlicenseduser;
+    /** 消息id,用于撤回应用消息 */
+    private String msgid;
+    /** 仅特定模板卡片消息返回,72小时内有效,且只能使用一次 */
+    private String responseCode;
+}

+ 94 - 0
src/main/java/com/qqflow/engine/domain/ewechat/service/EnterpriseWechatMsgService.java

@@ -0,0 +1,94 @@
+package com.qqflow.engine.domain.ewechat.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.qqflow.engine.domain.ewechat.api.EnterpriseWechatApi;
+import com.qqflow.engine.domain.ewechat.config.EnterpriseWechatProperties;
+import com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatCorpMapper;
+import com.qqflow.engine.domain.ewechat.model.enterprise.req.EnterpriseWechatSendTextMsgReq;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatCorp;
+import com.qqflow.engine.domain.system.entity.SysUser;
+import com.qqflow.engine.domain.system.entity.SysUserRole;
+import com.qqflow.engine.domain.system.mapper.SysUserMapper;
+import com.qqflow.engine.domain.system.mapper.SysUserRoleMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 企业微信消息发送服务
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class EnterpriseWechatMsgService {
+
+    private final EnterpriseWechatApi enterpriseWechatApi;
+    private final EnterpriseWechatProperties wechatProperties;
+    private final EnterpriseWechatCorpMapper wechatCorpMapper;
+    private final SysUserMapper sysUserMapper;
+    private final SysUserRoleMapper sysUserRoleMapper;
+
+    /** 给指定企微用户ID列表发送文本消息 */
+    public void sendTextToUsers(List<String> wecomUserIds, String content) {
+        if (wecomUserIds == null || wecomUserIds.isEmpty()) return;
+        String accessToken = this.getCorpAccessToken();
+        if (accessToken == null) {
+            log.warn("[企微消息] 无法获取 access_token");
+            return;
+        }
+        try {
+            EnterpriseWechatSendTextMsgReq req = EnterpriseWechatSendTextMsgReq.toUsers(
+                    wecomUserIds, wechatProperties.getAgentId(), content);
+            enterpriseWechatApi.sendTextMsg(accessToken, req);
+            log.info("[企微消息] 已发送 | 接收人数:{}", wecomUserIds.size());
+        } catch (Exception e) {
+            log.error("[企微消息] 发送失败", e);
+        }
+    }
+
+    /** 给指定角色下的所有企微用户发送文本消息 */
+    public void sendTextToRole(Long roleId, String content) {
+        List<String> wecomUserIds = this.getRoleWecomUserIds(roleId);
+        this.sendTextToUsers(wecomUserIds, content);
+    }
+
+    /** 给指定用户发送文本消息(如果该用户有企微账号) */
+    public void sendTextToUser(Long userId, String content) {
+        SysUser user = sysUserMapper.selectById(userId);
+        if (user != null && user.getWecomUserId() != null && !user.getWecomUserId().isBlank()) {
+            this.sendTextToUsers(Collections.singletonList(user.getWecomUserId()), content);
+        }
+    }
+
+    /** 获取角色下所有带企微账号的用户ID列表 */
+    public List<String> getRoleWecomUserIds(Long roleId) {
+        List<SysUserRole> userRoles = sysUserRoleMapper.selectList(
+                new LambdaQueryWrapper<SysUserRole>().eq(SysUserRole::getRoleId, roleId));
+        return userRoles.stream()
+                .map(ur -> sysUserMapper.selectById(ur.getUserId()))
+                .filter(u -> u != null && u.getWecomUserId() != null && !u.getWecomUserId().isBlank())
+                .map(SysUser::getWecomUserId)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
+    private String getCorpAccessToken() {
+        List<EnterpriseWechatCorp> corps = wechatCorpMapper.selectList(
+                new LambdaQueryWrapper<EnterpriseWechatCorp>()
+                        .eq(EnterpriseWechatCorp::getSuiteId, wechatProperties.getSuiteId())
+                        .isNotNull(EnterpriseWechatCorp::getPermanentCode)
+                        .last("LIMIT 1"));
+        if (corps.isEmpty()) return null;
+        EnterpriseWechatCorp corp = corps.get(0);
+        try {
+            return enterpriseWechatApi.getAccessToken(corp.getCorpId(), corp.getPermanentCode()).getAccessToken();
+        } catch (Exception e) {
+            log.error("[企微消息] 获取 access_token 失败, corpId={}", corp.getCorpId(), e);
+            return null;
+        }
+    }
+}

+ 24 - 97
src/main/java/com/qqflow/engine/domain/flow/service/impl/WeComNotificationService.java

@@ -1,5 +1,6 @@
 package com.qqflow.engine.domain.flow.service.impl;
 
+import com.qqflow.engine.domain.ewechat.service.EnterpriseWechatMsgService;
 import com.qqflow.engine.domain.flow.po.ApprovalTask;
 import com.qqflow.engine.domain.flow.service.NotificationService;
 import com.qqflow.engine.domain.system.dto.WeComConfigDTO;
@@ -12,22 +13,9 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
+import java.util.Collections;
 import java.util.List;
 
-/**
- * 企业微信通知服务(预留实现)
- * <p>
- * 本服务仅负责收集通知事件、读取企微配置、定位接收人的企微账号,并通过日志输出待发送内容。
- * 实际的企业微信应用消息推送由外部对接系统完成,对接方可选择以下方式消费:
- * 1. 监听本系统发布的 TaskAssignedEvent / TaskCompletedEvent / ProcessCompletedEvent;
- * 2. 调用本系统预留的查询接口(如 {@link com.qqflow.engine.domain.system.service.SysNotificationConfigService#getWeComConfig()})读取配置;
- * 3. 读取 sys_user.wecom_user_id 或 sys_role.wecom_user_id 获取接收人企微账号。
- * <p>
- * 对接时需要:
- * - 企业微信 corpId, agentId, secret(存储在 sys_notification_config 表)
- * - 用户/角色企微账号映射(sys_user.wecom_user_id / sys_role.wecom_user_id)
- * - 调用企微消息推送 API: POST https://qyapi.weixin.qq.com/cgi-bin/message/send
- */
 @Slf4j
 @Service
 @RequiredArgsConstructor
@@ -38,77 +26,48 @@ public class WeComNotificationService implements NotificationService {
     private final SysUserMapper sysUserMapper;
     private final SysRoleMapper sysRoleMapper;
     private final SysNotificationConfigService sysNotificationConfigService;
+    private final EnterpriseWechatMsgService wechatMsgService;
 
     @Override
     public void notifyTaskAssigned(List<Long> assigneeIds, String assigneeType, String processName, String instanceTitle, String nodeName) {
-        WeComConfigDTO config = this.sysNotificationConfigService.getWeComConfig();
-        if (!isEnabled(config)) {
-            log.debug("[企微通知-预留] 企业微信通知未启用,跳过待办提醒");
-            return;
-        }
+        if (!this.isEnabled()) return;
         for (Long assigneeId : assigneeIds) {
             String wecomUserId = resolveWeComUserId(assigneeId, assigneeType);
-            String receiverName = resolveReceiverName(assigneeId, assigneeType);
-            if (!hasReminderEnabled(assigneeId, assigneeType) || wecomUserId == null) {
-                continue;
-            }
-            log.info("[企微通知-预留] 待办提醒 | 接收人:{} | 企微账号:{} | 流程:{} | 标题:{} | 节点:{} | corpId:{} | agentId:{}",
-                    receiverName, wecomUserId, processName, instanceTitle, nodeName,
-                    mask(config.getCorpId()), mask(config.getAgentId()));
-            // TODO: 生产环境由企业微信对接后端调用企微 API 发送应用消息
+            if (wecomUserId == null) continue;
+            String content = String.format("【待办提醒】\n流程:%s\n标题:%s\n节点:%s\n请及时处理。",
+                    processName, instanceTitle, nodeName);
+            wechatMsgService.sendTextToUsers(Collections.singletonList(wecomUserId), content);
         }
     }
 
     @Override
     public void notifyTaskCompleted(Long instanceId, String processName, String instanceTitle,
                                     String nodeName, String operatorName, String action) {
-        WeComConfigDTO config = this.sysNotificationConfigService.getWeComConfig();
-        if (!isEnabled(config)) {
-            log.debug("[企微通知-预留] 企业微信通知未启用,跳过审批结果通知");
-            return;
-        }
-        log.info("[企微通知-预留] 审批结果 | 实例ID:{} | 流程:{} | 标题:{} | 节点:{} | 操作人:{} | 操作:{} | corpId:{} | agentId:{}",
-                instanceId, processName, instanceTitle, nodeName, operatorName, action,
-                mask(config.getCorpId()), mask(config.getAgentId()));
-        // TODO: 生产环境由企业微信对接后端调用企微 API 发送审批结果通知(通知发起人或相关人)
+        // 审批完成通知:目前仅记录日志,后续可扩展
     }
 
     @Override
     public void notifyProcessCompleted(Long applicantId, String processName, String instanceTitle, String result) {
-        WeComConfigDTO config = this.sysNotificationConfigService.getWeComConfig();
-        if (!isEnabled(config)) {
-            log.debug("[企微通知-预留] 企业微信通知未启用,跳过流程结束通知");
-            return;
-        }
+        if (!this.isEnabled()) return;
         SysUser applicant = sysUserMapper.selectById(applicantId);
-        if (applicant == null || !isUserRemindEnabled(applicant)) {
-            return;
+        if (applicant != null && applicant.getWecomUserId() != null && !applicant.getWecomUserId().isBlank()) {
+            String content = String.format("【流程结束】\n流程:%s\n标题:%s\n结果:%s",
+                    processName, instanceTitle, result);
+            wechatMsgService.sendTextToUsers(Collections.singletonList(applicant.getWecomUserId()), content);
         }
-        log.info("[企微通知-预留] 流程结束 | 接收人:{} | 企微账号:{} | 流程:{} | 标题:{} | 结果:{} | corpId:{} | agentId:{}",
-                applicant.getRealName(), applicant.getWecomUserId(), processName, instanceTitle, result,
-                mask(config.getCorpId()), mask(config.getAgentId()));
-        // TODO: 生产环境由企业微信对接后端调用企微 API 发送流程结束通知
     }
 
     @Override
     public void notifyTaskOverdue(ApprovalTask task, String processName, String instanceTitle, String nodeName, boolean overdue) {
-        WeComConfigDTO config = this.sysNotificationConfigService.getWeComConfig();
-        if (!isEnabled(config)) {
-            log.debug("[企微通知-预留] 企业微信通知未启用,跳过超时催办通知");
-            return;
-        }
-        Long assigneeId = task.getAssigneeId();
-        String assigneeType = task.getAssigneeType();
-        String wecomUserId = resolveWeComUserId(assigneeId, assigneeType);
-        String receiverName = resolveReceiverName(assigneeId, assigneeType);
-        if (!hasReminderEnabled(assigneeId, assigneeType) || wecomUserId == null) {
-            return;
-        }
+        if (!this.isEnabled()) return;
         String type = overdue ? "已超时催办" : "即将超时提醒";
-        log.info("[企微通知-预留] {} | 接收人:{} | 企微账号:{} | 流程:{} | 标题:{} | 节点:{} | corpId:{} | agentId:{}",
-                type, receiverName, wecomUserId, processName, instanceTitle, nodeName,
-                mask(config.getCorpId()), mask(config.getAgentId()));
-        // TODO: 生产环境由企业微信对接后端调用企微 API 发送超时/催办通知
+        String content = String.format("【%s】\n流程:%s\n标题:%s\n节点:%s\n请及时处理。",
+                type, processName, instanceTitle, nodeName);
+        if (ASSIGNEE_TYPE_ROLE.equalsIgnoreCase(task.getAssigneeType())) {
+            wechatMsgService.sendTextToRole(task.getAssigneeId(), content);
+        } else {
+            wechatMsgService.sendTextToUser(task.getAssigneeId(), content);
+        }
     }
 
     private String resolveWeComUserId(Long assigneeId, String assigneeType) {
@@ -120,40 +79,8 @@ public class WeComNotificationService implements NotificationService {
         return user != null ? user.getWecomUserId() : null;
     }
 
-    private String resolveReceiverName(Long assigneeId, String assigneeType) {
-        if (ASSIGNEE_TYPE_ROLE.equalsIgnoreCase(assigneeType)) {
-            SysRole role = sysRoleMapper.selectById(assigneeId);
-            return role != null ? role.getRoleName() : "";
-        }
-        SysUser user = sysUserMapper.selectById(assigneeId);
-        return user != null ? user.getRealName() : "";
-    }
-
-    private boolean hasReminderEnabled(Long assigneeId, String assigneeType) {
-        if (ASSIGNEE_TYPE_ROLE.equalsIgnoreCase(assigneeType)) {
-            SysRole role = sysRoleMapper.selectById(assigneeId);
-            return role != null && isRoleRemindEnabled(role);
-        }
-        SysUser user = sysUserMapper.selectById(assigneeId);
-        return user != null && isUserRemindEnabled(user);
-    }
-
-    private boolean isEnabled(WeComConfigDTO config) {
+    private boolean isEnabled() {
+        WeComConfigDTO config = sysNotificationConfigService.getWeComConfig();
         return config != null && config.getEnabled() != null && config.getEnabled() == 1;
     }
-
-    private boolean isUserRemindEnabled(SysUser user) {
-        return user.getWecomRemindEnabled() == null || user.getWecomRemindEnabled() == 1;
-    }
-
-    private boolean isRoleRemindEnabled(SysRole role) {
-        return role.getWecomRemindEnabled() == null || role.getWecomRemindEnabled() == 1;
-    }
-
-    private String mask(String value) {
-        if (value == null || value.length() <= 4) {
-            return value;
-        }
-        return value.substring(0, 2) + "****" + value.substring(value.length() - 2);
-    }
 }

+ 2 - 1
src/main/resources/application-test.yml

@@ -38,4 +38,5 @@ enterprise-wechat:
   token: jGgY9aLKA
   encoding-a-eSKey: IycRI8M3tgTIHrFiwWnYWJn9I1o8iIsQrhNy8M4MWnT
   suite-id: dk0580416972e402b8
-  suite-secret: 3aV48BTZHxDBeltZgdiRAUhttqsE5RxNs5pzElGhHHg
+  suite-secret: 3aV48BTZHxDBeltZgdiRAUhttqsE5RxNs5pzElGhHHg
+  agent-id: 1000095

+ 13 - 3
src/test/java/com/qqflow/engine/domain/ewechat/api/EnterpriseWechatApiTest.java

@@ -1,6 +1,7 @@
 package com.qqflow.engine.domain.ewechat.api;
 
 import com.qqflow.engine.common.util.JsonUtil;
+import com.qqflow.engine.domain.ewechat.model.enterprise.req.EnterpriseWechatSendTextMsgReq;
 import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.*;
 import com.qqflow.engine.domain.ewechat.service.EnterpriseWechatService;
 import org.junit.jupiter.api.Test;
@@ -9,8 +10,6 @@ import org.springframework.boot.test.context.SpringBootTest;
 
 import java.io.File;
 
-import static org.junit.jupiter.api.Assertions.*;
-
 @SpringBootTest
 class EnterpriseWechatApiTest {
     @Autowired
@@ -25,6 +24,8 @@ class EnterpriseWechatApiTest {
 
     private static final String suiteId = "dk0580416972e402b8";
 
+    private static final Integer agentId = 1000095;
+
     @Test
     void getAccessToken() {
         EnterpriseWechatAccessTokenRsp accessToken = enterpriseWechatApi.getAccessToken(corpId,"1avQqFtd-5xHqZpoktm4IeY-r4fuH9hcccYoi8mP1Hg");
@@ -81,8 +82,9 @@ class EnterpriseWechatApiTest {
     @Test
     void getUseridByMobile() {
         String accessToken = enterpriseWechatService.getAccessToken(corpId, suiteId);
-        EnterpriseWechatUseridByMobileRsp rsp = enterpriseWechatApi.getUseridByMobile(accessToken, "13450478314");
+        EnterpriseWechatUseridByMobileRsp rsp = enterpriseWechatApi.getUseridByMobile(accessToken, "18078824317");
         System.out.println(JsonUtil.entity2Json(rsp));
+        // woU17nDAAAb3FTGfOj_XhfNcBdxsFesQ ,员工 memberId
     }
 
 
@@ -92,4 +94,12 @@ class EnterpriseWechatApiTest {
         EnterpriseWechatListIdRsp rsp = enterpriseWechatApi.listUserId(accessToken, null, 10);
         System.out.println(JsonUtil.entity2Json(rsp));
     }
+
+    @Test
+    void sendMsg() {
+        String accessToken = enterpriseWechatService.getAccessToken(corpId, suiteId);
+        EnterpriseWechatSendTextMsgReq textMsgReq = EnterpriseWechatSendTextMsgReq.toUser("woU17nDAAAb3FTGfOj_XhfNcBdxsFesQ", agentId, "hello test");
+        EnterpriseWechatSendTextMsgRsp sendTextMsgRsp = enterpriseWechatApi.sendTextMsg(accessToken, textMsgReq);
+        System.out.println(JsonUtil.entity2Json(sendTextMsgRsp));
+    }
 }