使用MQ实现企业微信应用程序发送消息到指定人或部门
提前部署好RabbitMQ服务,不会的同学请看我的另一篇部署文章
引入相关依赖
pom.xml
<!-- RabbitMQ依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.7.3</version>
</dependency>
<!-- http调用依赖 -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot-starter</artifactId>
<version>1.5.25</version>
</dependency>
代码实现
controller
/**
* 控制层
*/
@RestController
@RequestMapping("/agent/msg")
public class AgentMsgController {
/**
* 服务对象
*/
@Resource
private AgentMsgService agentMsgService;
/**
* 新增消息
*/
@PostMapping("/add")
public Result<AgentMsg> add(@RequestBody AgentMsgDTO agentMsgDto) {
return R.ok(this.agentMsgService.insert(agentMsgDto));
}
}
AgentMsgDTO
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* 应用消息表(AgentMsg)DTO类
*/
@Data
public class AgentMsgDTO {
/**
* 主键ID
*/
private Long id;
/**
* 消息标题
*/
private String msgTitle;
/**
* 应用ID
*/
private Integer agentId;
/**
* 范围类型 1-全部 2-自定义
*/
private Integer scopeType;
/**
* 成员ID列表
*/
private List<String> toUser;
/**
* 部门ID列表
*/
private List<String> toParty;
/**
* 标签ID列表
*/
private List<String> toTag;
/**
* 发送方式 1-立即发送 2-定时发送
*/
private Integer sendType;
/**
* 发送时间
*/
private Date sendTime;
/**
* 计划时间
*/
private Date planSendTime;
/**
* 消息状态:0-草稿 1-待发送 2-已发送 3-发送失败 4-已撤回
*/
private Integer status;
/**
* 消息ID
*/
private String msgId;
/**
* 消息模板
*/
private WeComMessageTemplate messageTemplate;
}
WeComMessageTemplate
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* @description 应用消息通知入参
**/
@Data
public class WeComMessageTemplate {
/**
* 消息类型 文本:text, 图片:image, 语音:voice, 视频:video, 文件:file, 文本卡片:textcard, 图文:news, 图文消息:link, 小程序:miniprogram
*/
@NotNull(message = "消息类型不能为空")
private String msgType;
/**
* 文本内容(文本消息必传)
*/
private String content;
/**
* 素材id(语音、视频、文件 必传)
*/
private String mediaId;
/**
* 小程序封面media_id
*/
private String picMediaId;
/**
* 消息的标题(视频、文本卡片、图文 必传)
*/
private String title;
/**
* 消息的描述(视频、文本卡片、图文 必传)
*/
private String description;
/**
* 点击后跳转的链接。最长2048字节,请确保包含了协议头(http/https) (文本卡片、图文 必传)
*/
private String linkUrl;
/**
* 图文消息的图片链接,支持JPG、PNG格式,较好的效果为大图 1068*455,小图150*150。(文本卡片、图文 必传)
*/
private String picUrl;
/**
* 文件连接
*/
private String fileUrl;
/**
* 小程序appid(可以在微信公众平台上查询),必须是关联到企业的小程序应用
*/
private String appId;
}
service
/**
* 应用消息表(AgentMsg)表服务实现类
*/
@Service
public class AgentMsgServiceImpl extends ServiceImpl<AgentMsgMapper, AgentMsg> implements AgentMsgService {
@Resource
private WeComSendMsgService sendMsgService;
/**
* 新增数据
*
* @param agentMsgDto 实例对象
* @return 实例对象
*/
@Override
public AgentMsg insert(AgentMsgDTO agentMsgDto) {
AgentMsg agentMsg = new AgentMsg();
BeanCopyUtils.copyBean(agentMsgDto, agentMsg);
// 此处分立即发送和定时发送,定时发送使用xxl-job实现
if (save(agentMsg) && agentMsgDto.getStatus() == 1 && agentMsgDto.getSendType() == 1) {
// 立即发送
WeComAgentMsgBody agentMsgBody = new WeComAgentMsgBody();
agentMsgBody.setMessageTemplates(agentMsgDto.getMessageTemplate());
agentMsgBody.setBusinessType(WeComMsgTypeEnum.AGENT.getType());
agentMsgBody.setCorpId(SecurityUtils.getCorpId());
agentMsgBody.setCorpUserIds(agentMsgDto.getToUser());
agentMsgBody.setDeptIds(agentMsgDto.getToParty());
agentMsgBody.setTagIds(agentMsgDto.getToTag());
agentMsgBody.setCallBackId(agentMsg.getId());
sendMsgService.sendMsg(agentMsgBody);
}
return agentMsg;
}
WeComSendMsgService
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @description 企微应用通知
**/
@Slf4j
@Service
public class WeComSendMsgService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RabbitMQSettingConfig rabbitMQSettingConfig;
public void sendMsg(WeComAgentMsgBody body) {
String json = JSONObject.toJSONString(body);
log.info("发送应用通知消息到MQ入参:{}", json);
rabbitTemplate.convertAndSend(rabbitMQSettingConfig.getWeAppMsgEx(), rabbitMQSettingConfig.getWeAppMsgRk(), json);
}
}
消息监听消费:WeComMsgListener
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* 企微应用消息监听
**/
@Slf4j
@Component
public class WeComMsgListener {
@RabbitHandler
@RabbitListener(queues = "${wecom.mq.queue.app-msg:Qu_AppMsg}")
public void subscribe(String msg, Channel channel, Message message) {
try {
log.info("应用通知消息监听:msg:{}", msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
WeComAgentMsgBody appMsgBody = JSONObject.parseObject(msg, WeComAgentMsgBody.class);
WeComMsgTypeEnum msgTypeEnum = WeComMsgTypeEnum.parseEnum(appMsgBody.getBusinessType());
if (Objects.nonNull(msgTypeEnum)) {
if (msgTypeEnum == WeComMsgTypeEnum.AGENT) {
SpringUtils.getBean(msgTypeEnum.getBeanName(), AbstractWeComMsgService.class).sendAgentMsg(appMsgBody);
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("应用通知消息监听-消息处理失败 msg:{},error:{}", msg, e);
}
}
}
AbstractWeComMsgService
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 消息发送
**/
@Service
@Slf4j
public abstract class AbstractWeComMsgService {
@Resource
private WeComAgentClient agentClient;
@Autowired
private CorpAccountService corpAccountService;
/**
* 消息结果处理
*/
protected void callBackResult(WeComMsgVo appMsgVo) {
}
/**
* 获取消息体
*/
protected abstract WeComAgentMsgDto getWeComAgentMsg(WeComAgentMsgBody agentMsgBody);
public void sendAgentMsg(WeComAgentMsgBody agentMsgBody) {
try {
WeComAgentMsgDto agentMsg = getWeComAgentMsg(agentMsgBody);
WeComMsgVo result = agentClient.sendAgentMsg(agentMsg);
callBackResult(result);
} catch (Exception e) {
log.error("sendAgentMsg 执行异常: query:{}", JSONObject.toJSONString(agentMsgBody), e);
WeComMsgVo errorBody = new WeComMsgVo();
errorBody.setErrMsg(e.getMessage());
errorBody.setErrCode(-1);
callBackResult(errorBody);
}
}
}
WeComAgentMsgServiceImpl
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;
import java.util.Objects;
/**
* 应用消息处理
**/
@Slf4j
@Service("WeComAgentMsgService")
public class WeComAgentMsgServiceImpl extends AbstractWeComMsgService {
private Long callBackId;
@Autowired
private AgentMsgService agentMsgService;
@Resource
private WeComMediaClient mediaClient;
@Override
protected WeComAgentMsgDto getWeComAgentMsg(WeComAgentMsgBody agentMsgBody) {
callBackId = agentMsgBody.getCallBackId();
AgentMsg weAgentMsg = agentMsgService.getById(agentMsgBody.getCallBackId());
if (Objects.isNull(weAgentMsg)) {
throw new WeComException("未找到对应消息数据");
}
WeComAgentMsgDto query = new WeComAgentMsgDto();
query.setAgentid(String.valueOf(weAgentMsg.getAgentId()));
if (CollectionUtil.isNotEmpty(agentMsgBody.getCorpUserIds())) {
query.setTouser(String.join("|", agentMsgBody.getCorpUserIds()));
} else if (CollectionUtil.isNotEmpty(agentMsgBody.getDeptIds())) {
query.setToparty(String.join("|", agentMsgBody.getDeptIds()));
} else if (CollectionUtil.isNotEmpty(agentMsgBody.getTagIds())) {
query.setTotag(String.join("|", agentMsgBody.getTagIds()));
}
WeComMessageTemplate messageTemplates = agentMsgBody.getMessageTemplates();
// 消息类型判断,此处只拿text演示
if (MessageType.TEXT.getMessageType().equals(messageTemplates.getMsgType())) {
query.setMsgtype(MessageType.TEXT.getMessageType());
query.setText(WeComAgentMsgDto.Text.builder().content(messageTemplates.getContent()).build());
}
return query;
}
/**
* 返回结果保存
*
* @param appMsgVo 返回结果
*/
@Override
protected void callBackResult(Long id, WeComMsgVo appMsgVo) {
log.info(">>>>>>【任务发送结果】 消息Id:{}, 返回结果:{}", id, JSONObject.toJSONString(appMsgVo));
AgentMsg weAgentMsg = new AgentMsg();
weAgentMsg.setId(id);
weAgentMsg.setInvalidUser(appMsgVo.getInvalidUser());
weAgentMsg.setInvalidParty(appMsgVo.getInvalidParty());
weAgentMsg.setInvalidTag(appMsgVo.getInvalidTag());
weAgentMsg.setUnlicensedUser(appMsgVo.getUnlicenseduser());
weAgentMsg.setMsgId(appMsgVo.getMsgId());
weAgentMsg.setResponseCode(appMsgVo.getResponse_code());
weAgentMsg.setSendTime(new Date());
if (appMsgVo.getErrMsg() != null && !Objects.equals(0, appMsgVo.getErrCode())) {
weAgentMsg.setStatus(3);
} else {
weAgentMsg.setStatus(2);
}
agentMsgService.updateById(weAgentMsg);
}
}
企微连接:WeComAgentClient
import com.dtflys.forest.annotation.*;
import org.springframework.web.bind.annotation.RequestBody;
/**
* 企微应用管理
*/
@BaseRequest(baseURL = "https://qyapi.weixin.qq.com/cgi-bin", interceptor = WeComAgentTokenInterceptor.class)
@Retry(maxRetryCount = "3", maxRetryInterval = "1000")
public interface WeComAgentClient {
/**
* 应用消息通知
*/
@Post("/message/send")
WeComMsgVo sendAgentMsg(@JSONBody WeComAgentMsgDto query);
}
拦截器:WeComAgentTokenInterceptor
获取access_token
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.dtflys.forest.exceptions.ForestRuntimeException;
import com.dtflys.forest.http.ForestRequest;
import com.dtflys.forest.http.ForestResponse;
import com.dtflys.forest.interceptor.Interceptor;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* @description: 应用token
* @author: seven
* @create: 2022-11-03 22:36
**/
@Slf4j
@Component
public class WeComAgentTokenInterceptor extends WeComForestInterceptor implements Interceptor<WeComResultVo> {
/**
* 该方法在请求发送之前被调用, 若返回false则不会继续发送请求
*/
@Override
public boolean beforeExecute(ForestRequest request) {
if (accessTokenService == null) {
accessTokenService = SpringUtils.getBean(WeComAccessTokenService.class);
}
Integer agentId = null;
Object[] arguments = request.getArguments();
for (Object argument : arguments) {
if (argument instanceof WeComBaseDto) {
WeComBaseDto query = (WeComBaseDto) argument;
agentId = Integer.valueOf(query.getAgentid());
}
}
if (Objects.isNull(agentId)) {
throw new BadRequestException("应用编号为空,请设置应用编号!");
}
String token = accessTokenService.getAgentAccessToken(getCorpId(), agentId);
request.replaceOrAddQuery("access_token", token);
return true;
}
/**
* 请求发送失败时被调用
*/
@Override
public void onError(ForestRuntimeException e, ForestRequest forestRequest, ForestResponse forestResponse) {
log.info("onError url:{},------params:{},----------result:{}", forestRequest.getUrl(), JSONObject.toJSONString(forestRequest.getArguments()), forestResponse.getContent());
if (StringUtils.isNotEmpty(forestResponse.getContent())) {
throw new BadRequestException(forestResponse.getContent());
} else {
throw new BadRequestException("网络请求超时");
}
}
/**
* 请求成功调用(微信端错误异常统一处理)
*/
@Override
public void onSuccess(WeComResultVo resultDto, ForestRequest forestRequest, ForestResponse forestResponse) {
log.info("onSuccess url:{},result:{}", forestRequest.getUrl(), forestResponse.getContent());
}
/**
* 请求重试
*/
@Override
public void onRetry(ForestRequest request, ForestResponse response) {
log.info("url:{}, query:{},params:{}, 重试原因:{}, 当前重试次数:{}", request.getUrl(), request.getQueryString(), JSONObject.toJSONString(request.getArguments()), response.getContent(), request.getCurrentRetryCount());
WeComResultVo weComResultVo = JSONUtil.toBean(response.getContent(), WeComResultVo.class);
// 刷新token
if (!ObjectUtil.equal(WeErrorCodeEnum.ERROR_CODE_OWE_1.getErrorCode(), weComResultVo.getErrCode()) && Lists.newArrayList(errorCodeRetry).contains(weComResultVo.getErrCode().toString())) {
// 删除缓存
Integer agentId = null;
Object[] arguments = request.getArguments();
for (Object argument : arguments) {
if (argument instanceof WeComBaseQuery) {
WeComBaseQuery query = (WeComBaseQuery) argument;
agentId = Integer.valueOf(query.getAgentid());
}
}
if (Objects.isNull(agentId)) {
throw new BadRequestException("应用编号为空,请设置应用编号!");
}
accessTokenService.removeAgentAccessToken(getCorpId(), agentId);
String token = accessTokenService.getAgentAccessToken(getCorpId(), agentId);
request.replaceOrAddQuery("access_token", token);
}
}
}
WeComForestInterceptor
获取企业id
import io.seata.common.util.CollectionUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 响应文件流
*/
@Slf4j
@Component
public abstract class WeComForestInterceptor {
@Autowired
protected WeComAccessTokenService accessTokenService;
@Autowired
protected CorpAccountService corpAccountService;
@Value("${wecom.error-code-retry}")
protected String errorCodeRetry;
protected String getCorpId() {
List<CorpAccount> list = corpAccountService.list();
if (CollectionUtils.isEmpty(list)) {
throw new BadRequestException("企业信息获取失败,请先完善企业信息!");
}
return list.get(0).getCorpId();
}
}
token获取业务类:WeComAccessTokenServiceImpl
package com.budsoft.scrm.service.wecom.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.budsoft.core.exception.BadRequestException;
import com.budsoft.scrm.client.WeComTokenClient;
import com.budsoft.scrm.common.utils.RedisUtils;
import com.budsoft.scrm.common.utils.StringUtils;
import com.budsoft.scrm.constant.WeComConstants;
import com.budsoft.scrm.domain.agent.entity.AgentInfo;
import com.budsoft.scrm.domain.entity.CorpAccount;
import com.budsoft.scrm.domain.wecom.vo.WeComAccessTokenVo;
import com.budsoft.scrm.service.CorpAccountService;
import com.budsoft.scrm.service.agent.AgentInfoService;
import com.budsoft.scrm.service.wecom.WeComAccessTokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @description: 获取企微token相关接口
* @author: seven
* @create: 2022-08-26 14:43
**/
@Service
public class WeComAccessTokenServiceImpl implements WeComAccessTokenService {
@Resource
private WeComTokenClient weComTokenClient;
@Autowired
private CorpAccountService corpAccountService;
@Autowired
private RedisUtils redisUtils;
@Autowired
private AgentInfoService agentInfoService;
@Override
public String getAgentAccessToken(String corpId, Integer agentId) {
String agentTokenKey = StringUtils.format(WeComConstants.WE_AGENT_ACCESS_TOKEN, corpId, agentId);
String accessToken = redisUtils.getCacheObject(agentTokenKey);
// 为空,请求微信服务器同时缓存到redis中
if (StringUtils.isEmpty(accessToken)) {
AgentInfo agentInfo = agentInfoService.getOne(new QueryWrapper<AgentInfo>().eq("agent_id", agentId));
if (Objects.isNull(agentInfo)) {
throw new BadRequestException("无可用的应用");
}
WeComAccessTokenVo weComAccessTokenVo = weComTokenClient.getToken(corpId, agentInfo.getSecret());
if (Objects.nonNull(weComAccessTokenVo) && StringUtils.isNotEmpty(weComAccessTokenVo.getAccessToken())) {
accessToken = weComAccessTokenVo.getAccessToken();
redisUtils.setCacheObject(agentTokenKey, weComAccessTokenVo.getAccessToken(), weComAccessTokenVo.getExpiresIn(), TimeUnit.SECONDS);
}
}
return accessToken;
}
@Override
public void removeAgentAccessToken(String corpId, Integer agentId) {
redisUtils.deleteObject(StringUtils.format(WeComConstants.WE_AGENT_ACCESS_TOKEN, corpId, agentId));
}
}
WeComTokenClient
import com.dtflys.forest.annotation.BaseRequest;
import com.dtflys.forest.annotation.Query;
import com.dtflys.forest.annotation.Request;
import com.dtflys.forest.annotation.Retry;
/**
* @author seven
* @description 获取企业微信token接口
* @date 2021/12/13 10:30
**/
@BaseRequest(baseURL = "https://qyapi.weixin.qq.com/cgi-bin")
@Retry(maxRetryCount = "3", maxRetryInterval = "1000")
public interface WeComTokenClient {
/**
* 获取token
*
* @param corpId 企业ID
* @param secret 企业密钥
* @return WeCorpTokenVo
*/
@Request(url = "/gettoken", type = "GET", interceptor = WeComNoTokenInterceptor.class)
WeComAccessTokenVo getToken(@Query("corpid") String corpId, @Query("corpsecret") String secret);
}