| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- package com.qqflow.engine.domain.ewechat.service;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.qqflow.engine.common.service.RedissonService;
- 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.EnterpriseWechatSendTextCardMsgReq;
- import com.qqflow.engine.domain.ewechat.model.enterprise.req.EnterpriseWechatSendTextMsgReq;
- import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.EnterpriseWechatSendTextMsgRsp;
- 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.time.Duration;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Map;
- 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;
- private final RedissonService redissonService;
- private static final String ACCESS_TOKEN_CACHE_KEY = "wecom:access_token:";
- /** access_token 有效期 7200 秒,缓存 7000 秒留余量 */
- private static final Duration ACCESS_TOKEN_TTL = Duration.ofSeconds(7000);
- /** 给指定企微用户ID列表发送文本消息;返回是否被企微接口成功接收 */
- public boolean sendTextToUsers(List<String> wecomUserIds, String content) {
- if (wecomUserIds == null || wecomUserIds.isEmpty()) return false;
- boolean allOk = true;
- // 多企业部署:接收人按所属企业分组,各自用本企业的 token + agentid 发送
- for (Map.Entry<String, List<String>> entry : groupUserIdsByCorp(wecomUserIds).entrySet()) {
- EnterpriseWechatCorp corp = this.getCorpById(entry.getKey());
- List<String> corpUserIds = entry.getValue();
- String accessToken = this.getCorpAccessToken(corp);
- if (accessToken == null) {
- log.warn("[企微消息] 无法获取企业 {} 的 access_token,跳过 {} 人", entry.getKey(), corpUserIds.size());
- allOk = false;
- continue;
- }
- try {
- EnterpriseWechatSendTextMsgReq req = EnterpriseWechatSendTextMsgReq.toUsers(
- corpUserIds, this.resolveAgentId(corp), content);
- EnterpriseWechatSendTextMsgRsp rsp = enterpriseWechatApi.sendTextMsg(accessToken, req);
- allOk &= logSendResult(rsp, "文本消息", corpUserIds.size());
- } catch (Exception e) {
- log.error("[企微消息] 发送失败 corpId={}", entry.getKey(), e);
- allOk = false;
- }
- }
- return allOk;
- }
- /** 给指定企微用户ID列表发送文本卡片消息(可点击跳转,用于审批待办/催办);返回是否发送成功 */
- public boolean sendTextCardToUsers(List<String> wecomUserIds, String title, String description, String url, String btnText) {
- if (wecomUserIds == null || wecomUserIds.isEmpty()) return false;
- boolean allOk = true;
- // 多企业部署:接收人按所属企业分组发送,卡片链接携带 corpId 供 H5 免登按企业路由
- for (Map.Entry<String, List<String>> entry : groupUserIdsByCorp(wecomUserIds).entrySet()) {
- EnterpriseWechatCorp corp = this.getCorpById(entry.getKey());
- List<String> corpUserIds = entry.getValue();
- String accessToken = this.getCorpAccessToken(corp);
- if (accessToken == null) {
- log.warn("[企微消息] 无法获取企业 {} 的 access_token,跳过 {} 人", entry.getKey(), corpUserIds.size());
- allOk = false;
- continue;
- }
- try {
- EnterpriseWechatSendTextCardMsgReq req = EnterpriseWechatSendTextCardMsgReq.toUsers(
- corpUserIds, this.resolveAgentId(corp), title, description,
- appendCorpIdToUrl(url, entry.getKey()), btnText);
- EnterpriseWechatSendTextMsgRsp rsp = enterpriseWechatApi.sendTextCardMsg(accessToken, req);
- allOk &= logSendResult(rsp, "文本卡片", corpUserIds.size());
- } catch (Exception e) {
- log.error("[企微消息] 文本卡片发送失败 corpId={}", entry.getKey(), e);
- allOk = false;
- }
- }
- return allOk;
- }
- /** 接收人按所属企业分组:wecom_user_id → sys_user.corp_id;无记录/为空 → 默认授权企业 */
- private Map<String, List<String>> groupUserIdsByCorp(List<String> wecomUserIds) {
- List<String> distinct = wecomUserIds.stream().distinct().collect(Collectors.toList());
- Map<String, String> userCorpMap = new HashMap<>();
- sysUserMapper.selectBatchByWecomUserIds(distinct).forEach(u ->
- userCorpMap.putIfAbsent(u.getWecomUserId(), u.getCorpId()));
- EnterpriseWechatCorp defaultCorp = this.getAuthorizedCorp();
- String defaultCorpId = defaultCorp == null ? null : defaultCorp.getCorpId();
- Map<String, List<String>> grouped = new LinkedHashMap<>();
- for (String userid : wecomUserIds) {
- String corpId = userCorpMap.getOrDefault(userid, defaultCorpId);
- grouped.computeIfAbsent(corpId, k -> new ArrayList<>()).add(userid);
- }
- return grouped;
- }
- /** 按 corp_id 查企业;为空回退默认授权企业 */
- public EnterpriseWechatCorp getCorpById(String corpId) {
- if (corpId == null || corpId.isBlank()) {
- return this.getAuthorizedCorp();
- }
- return wechatCorpMapper.selectByCorpId(corpId);
- }
- /** 发送用的应用 agentid:优先企业自身(代开发各企业 agentid 不同),为空回退全局配置 */
- public Integer resolveAgentId(EnterpriseWechatCorp corp) {
- return corp != null && corp.getAgentId() != null ? corp.getAgentId() : wechatProperties.getAgentId();
- }
- /** 卡片链接追加 corpId 参数(H5 免登按企业取 appid/token 用) */
- private String appendCorpIdToUrl(String url, String corpId) {
- if (url == null || corpId == null || corpId.isBlank()) {
- return url;
- }
- return url + (url.contains("?") ? "&" : "?") + "corpId=" + corpId;
- }
- /**
- * 记录企微接口真实返回码:errcode=0 才算成功。
- * 之前只看 HTTP 异常,errcode 非 0(如 60020 IP 不在白名单)会被当成发送成功。
- */
- private boolean logSendResult(EnterpriseWechatSendTextMsgRsp rsp, String scene, int userCount) {
- if (rsp == null) {
- log.error("[企微消息] {}无响应 | 接收人数:{}", scene, userCount);
- return false;
- }
- if (rsp.getErrcode() == null || rsp.getErrcode() == 0) {
- log.info("[企微消息] {}已发送 | 接收人数:{} | msgid:{}", scene, userCount, rsp.getMsgid());
- return true;
- }
- log.error("[企微消息] {}发送失败 | 接收人数:{} | errcode={} errmsg={} invaliduser={}",
- scene, userCount, rsp.getErrcode(), rsp.getErrmsg(), rsp.getInvaliduser());
- return false;
- }
- /** 给指定角色下的所有企微用户发送文本消息 */
- 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());
- }
- /** 当前已授权的代开发企业(取第一条有永久授权码的,单企业部署场景) */
- public EnterpriseWechatCorp getAuthorizedCorp() {
- List<EnterpriseWechatCorp> corps = wechatCorpMapper.selectList(
- new LambdaQueryWrapper<EnterpriseWechatCorp>()
- .eq(EnterpriseWechatCorp::getSuiteId, wechatProperties.getSuiteId())
- .isNotNull(EnterpriseWechatCorp::getPermanentCode)
- .last("LIMIT 1"));
- return corps.isEmpty() ? null : corps.get(0);
- }
- /** 获取默认授权企业的 access_token(未绑定企业的用户/内部调用使用) */
- public String getCorpAccessToken() {
- return this.getCorpAccessToken(this.getAuthorizedCorp());
- }
- /** 获取指定企业的 access_token(Redis 按 corp_id 缓存,TTL 7000s) */
- public String getCorpAccessToken(EnterpriseWechatCorp corp) {
- if (corp == null) return null;
- String cacheKey = ACCESS_TOKEN_CACHE_KEY + corp.getCorpId();
- String cached = redissonService.get(cacheKey);
- if (cached != null) return cached;
- try {
- String token = enterpriseWechatApi.getAccessToken(corp.getCorpId(), corp.getPermanentCode()).getAccessToken();
- redissonService.set(cacheKey, token, ACCESS_TOKEN_TTL);
- return token;
- } catch (Exception e) {
- log.error("[企微消息] 获取 access_token 失败, corpId={}", corp.getCorpId(), e);
- return null;
- }
- }
- }
|