EnterpriseWechatMsgService.java 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. package com.qqflow.engine.domain.ewechat.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.qqflow.engine.common.service.RedissonService;
  4. import com.qqflow.engine.domain.ewechat.api.EnterpriseWechatApi;
  5. import com.qqflow.engine.domain.ewechat.config.EnterpriseWechatProperties;
  6. import com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatCorpMapper;
  7. import com.qqflow.engine.domain.ewechat.model.enterprise.req.EnterpriseWechatSendTextCardMsgReq;
  8. import com.qqflow.engine.domain.ewechat.model.enterprise.req.EnterpriseWechatSendTextMsgReq;
  9. import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.EnterpriseWechatSendTextMsgRsp;
  10. import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatCorp;
  11. import com.qqflow.engine.domain.system.entity.SysUser;
  12. import com.qqflow.engine.domain.system.entity.SysUserRole;
  13. import com.qqflow.engine.domain.system.mapper.SysUserMapper;
  14. import com.qqflow.engine.domain.system.mapper.SysUserRoleMapper;
  15. import lombok.RequiredArgsConstructor;
  16. import lombok.extern.slf4j.Slf4j;
  17. import org.springframework.stereotype.Service;
  18. import java.time.Duration;
  19. import java.util.ArrayList;
  20. import java.util.Collections;
  21. import java.util.HashMap;
  22. import java.util.LinkedHashMap;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.stream.Collectors;
  26. /**
  27. * 企业微信消息发送服务
  28. */
  29. @Slf4j
  30. @Service
  31. @RequiredArgsConstructor
  32. public class EnterpriseWechatMsgService {
  33. private final EnterpriseWechatApi enterpriseWechatApi;
  34. private final EnterpriseWechatProperties wechatProperties;
  35. private final EnterpriseWechatCorpMapper wechatCorpMapper;
  36. private final SysUserMapper sysUserMapper;
  37. private final SysUserRoleMapper sysUserRoleMapper;
  38. private final RedissonService redissonService;
  39. private static final String ACCESS_TOKEN_CACHE_KEY = "wecom:access_token:";
  40. /** access_token 有效期 7200 秒,缓存 7000 秒留余量 */
  41. private static final Duration ACCESS_TOKEN_TTL = Duration.ofSeconds(7000);
  42. /** 给指定企微用户ID列表发送文本消息;返回是否被企微接口成功接收 */
  43. public boolean sendTextToUsers(List<String> wecomUserIds, String content) {
  44. if (wecomUserIds == null || wecomUserIds.isEmpty()) return false;
  45. boolean allOk = true;
  46. // 多企业部署:接收人按所属企业分组,各自用本企业的 token + agentid 发送
  47. for (Map.Entry<String, List<String>> entry : groupUserIdsByCorp(wecomUserIds).entrySet()) {
  48. EnterpriseWechatCorp corp = this.getCorpById(entry.getKey());
  49. List<String> corpUserIds = entry.getValue();
  50. String accessToken = this.getCorpAccessToken(corp);
  51. if (accessToken == null) {
  52. log.warn("[企微消息] 无法获取企业 {} 的 access_token,跳过 {} 人", entry.getKey(), corpUserIds.size());
  53. allOk = false;
  54. continue;
  55. }
  56. try {
  57. EnterpriseWechatSendTextMsgReq req = EnterpriseWechatSendTextMsgReq.toUsers(
  58. corpUserIds, this.resolveAgentId(corp), content);
  59. EnterpriseWechatSendTextMsgRsp rsp = enterpriseWechatApi.sendTextMsg(accessToken, req);
  60. allOk &= logSendResult(rsp, "文本消息", corpUserIds.size());
  61. } catch (Exception e) {
  62. log.error("[企微消息] 发送失败 corpId={}", entry.getKey(), e);
  63. allOk = false;
  64. }
  65. }
  66. return allOk;
  67. }
  68. /** 给指定企微用户ID列表发送文本卡片消息(可点击跳转,用于审批待办/催办);返回是否发送成功 */
  69. public boolean sendTextCardToUsers(List<String> wecomUserIds, String title, String description, String url, String btnText) {
  70. if (wecomUserIds == null || wecomUserIds.isEmpty()) return false;
  71. boolean allOk = true;
  72. // 多企业部署:接收人按所属企业分组发送,卡片链接携带 corpId 供 H5 免登按企业路由
  73. for (Map.Entry<String, List<String>> entry : groupUserIdsByCorp(wecomUserIds).entrySet()) {
  74. EnterpriseWechatCorp corp = this.getCorpById(entry.getKey());
  75. List<String> corpUserIds = entry.getValue();
  76. String accessToken = this.getCorpAccessToken(corp);
  77. if (accessToken == null) {
  78. log.warn("[企微消息] 无法获取企业 {} 的 access_token,跳过 {} 人", entry.getKey(), corpUserIds.size());
  79. allOk = false;
  80. continue;
  81. }
  82. try {
  83. EnterpriseWechatSendTextCardMsgReq req = EnterpriseWechatSendTextCardMsgReq.toUsers(
  84. corpUserIds, this.resolveAgentId(corp), title, description,
  85. appendCorpIdToUrl(url, entry.getKey()), btnText);
  86. EnterpriseWechatSendTextMsgRsp rsp = enterpriseWechatApi.sendTextCardMsg(accessToken, req);
  87. allOk &= logSendResult(rsp, "文本卡片", corpUserIds.size());
  88. } catch (Exception e) {
  89. log.error("[企微消息] 文本卡片发送失败 corpId={}", entry.getKey(), e);
  90. allOk = false;
  91. }
  92. }
  93. return allOk;
  94. }
  95. /** 接收人按所属企业分组:wecom_user_id → sys_user.corp_id;无记录/为空 → 默认授权企业 */
  96. private Map<String, List<String>> groupUserIdsByCorp(List<String> wecomUserIds) {
  97. List<String> distinct = wecomUserIds.stream().distinct().collect(Collectors.toList());
  98. Map<String, String> userCorpMap = new HashMap<>();
  99. sysUserMapper.selectBatchByWecomUserIds(distinct).forEach(u ->
  100. userCorpMap.putIfAbsent(u.getWecomUserId(), u.getCorpId()));
  101. EnterpriseWechatCorp defaultCorp = this.getAuthorizedCorp();
  102. String defaultCorpId = defaultCorp == null ? null : defaultCorp.getCorpId();
  103. Map<String, List<String>> grouped = new LinkedHashMap<>();
  104. for (String userid : wecomUserIds) {
  105. String corpId = userCorpMap.getOrDefault(userid, defaultCorpId);
  106. grouped.computeIfAbsent(corpId, k -> new ArrayList<>()).add(userid);
  107. }
  108. return grouped;
  109. }
  110. /** 按 corp_id 查企业;为空回退默认授权企业 */
  111. public EnterpriseWechatCorp getCorpById(String corpId) {
  112. if (corpId == null || corpId.isBlank()) {
  113. return this.getAuthorizedCorp();
  114. }
  115. return wechatCorpMapper.selectByCorpId(corpId);
  116. }
  117. /** 发送用的应用 agentid:优先企业自身(代开发各企业 agentid 不同),为空回退全局配置 */
  118. public Integer resolveAgentId(EnterpriseWechatCorp corp) {
  119. return corp != null && corp.getAgentId() != null ? corp.getAgentId() : wechatProperties.getAgentId();
  120. }
  121. /** 卡片链接追加 corpId 参数(H5 免登按企业取 appid/token 用) */
  122. private String appendCorpIdToUrl(String url, String corpId) {
  123. if (url == null || corpId == null || corpId.isBlank()) {
  124. return url;
  125. }
  126. return url + (url.contains("?") ? "&" : "?") + "corpId=" + corpId;
  127. }
  128. /**
  129. * 记录企微接口真实返回码:errcode=0 才算成功。
  130. * 之前只看 HTTP 异常,errcode 非 0(如 60020 IP 不在白名单)会被当成发送成功。
  131. */
  132. private boolean logSendResult(EnterpriseWechatSendTextMsgRsp rsp, String scene, int userCount) {
  133. if (rsp == null) {
  134. log.error("[企微消息] {}无响应 | 接收人数:{}", scene, userCount);
  135. return false;
  136. }
  137. if (rsp.getErrcode() == null || rsp.getErrcode() == 0) {
  138. log.info("[企微消息] {}已发送 | 接收人数:{} | msgid:{}", scene, userCount, rsp.getMsgid());
  139. return true;
  140. }
  141. log.error("[企微消息] {}发送失败 | 接收人数:{} | errcode={} errmsg={} invaliduser={}",
  142. scene, userCount, rsp.getErrcode(), rsp.getErrmsg(), rsp.getInvaliduser());
  143. return false;
  144. }
  145. /** 给指定角色下的所有企微用户发送文本消息 */
  146. public void sendTextToRole(Long roleId, String content) {
  147. List<String> wecomUserIds = this.getRoleWecomUserIds(roleId);
  148. this.sendTextToUsers(wecomUserIds, content);
  149. }
  150. /** 给指定用户发送文本消息(如果该用户有企微账号) */
  151. public void sendTextToUser(Long userId, String content) {
  152. SysUser user = sysUserMapper.selectById(userId);
  153. if (user != null && user.getWecomUserId() != null && !user.getWecomUserId().isBlank()) {
  154. this.sendTextToUsers(Collections.singletonList(user.getWecomUserId()), content);
  155. }
  156. }
  157. /** 获取角色下所有带企微账号的用户ID列表 */
  158. public List<String> getRoleWecomUserIds(Long roleId) {
  159. List<SysUserRole> userRoles = sysUserRoleMapper.selectList(
  160. new LambdaQueryWrapper<SysUserRole>().eq(SysUserRole::getRoleId, roleId));
  161. return userRoles.stream()
  162. .map(ur -> sysUserMapper.selectById(ur.getUserId()))
  163. .filter(u -> u != null && u.getWecomUserId() != null && !u.getWecomUserId().isBlank())
  164. .map(SysUser::getWecomUserId)
  165. .distinct()
  166. .collect(Collectors.toList());
  167. }
  168. /** 当前已授权的代开发企业(取第一条有永久授权码的,单企业部署场景) */
  169. public EnterpriseWechatCorp getAuthorizedCorp() {
  170. List<EnterpriseWechatCorp> corps = wechatCorpMapper.selectList(
  171. new LambdaQueryWrapper<EnterpriseWechatCorp>()
  172. .eq(EnterpriseWechatCorp::getSuiteId, wechatProperties.getSuiteId())
  173. .isNotNull(EnterpriseWechatCorp::getPermanentCode)
  174. .last("LIMIT 1"));
  175. return corps.isEmpty() ? null : corps.get(0);
  176. }
  177. /** 获取默认授权企业的 access_token(未绑定企业的用户/内部调用使用) */
  178. public String getCorpAccessToken() {
  179. return this.getCorpAccessToken(this.getAuthorizedCorp());
  180. }
  181. /** 获取指定企业的 access_token(Redis 按 corp_id 缓存,TTL 7000s) */
  182. public String getCorpAccessToken(EnterpriseWechatCorp corp) {
  183. if (corp == null) return null;
  184. String cacheKey = ACCESS_TOKEN_CACHE_KEY + corp.getCorpId();
  185. String cached = redissonService.get(cacheKey);
  186. if (cached != null) return cached;
  187. try {
  188. String token = enterpriseWechatApi.getAccessToken(corp.getCorpId(), corp.getPermanentCode()).getAccessToken();
  189. redissonService.set(cacheKey, token, ACCESS_TOKEN_TTL);
  190. return token;
  191. } catch (Exception e) {
  192. log.error("[企微消息] 获取 access_token 失败, corpId={}", corp.getCorpId(), e);
  193. return null;
  194. }
  195. }
  196. }