之前使用阿里平台的短信和邮件发送服务,现在需要根据发送回执记录用户是否真实收到消息推送。查看文档发现可以通过消息服务MNS来实现,在此记录一下相关经验。
邮件消息回执
官方文档地址:https://help.aliyun.com/document_detail/52048.html
阿里云控制台配置:
开启MNS消息服务,
进入队列页面创建队列(接收回执消息使用)
进入主题页面创建主题,用于接收回执消息(阿里的消息回执是将回执消息推送到对应的主题,再通过订阅该主题的队列拉取回执消息,这里还需要注意一点,邮件的四个事件中,投递成功、投递失败是实时推送的,打开事件和点击事件是每个小时定时推送一次,队列才能拉取到该事件的消息,所以会出现延迟)
创建事件通知规则(投递成功、投递失败、打开、点击,四种事件规则,事件通知之间规则不能重复)
进入邮件推送服务,异步通知设置,我因为需要区分各环境之间消息,环境消息区分与短信回执相同只能自行在业务层面处理,所以我选择发信地址级异步通知,每个环境一个发信地址,通过回执消息中发信地址判断是否为当前环境消息。
关于发信地址级和账户级异步通知区别说明
1. 发信地址级异步通知优先级高于账户级异步通知。既设置账户级,又设置发信地址级异步通知,则账户级设置对该发信地址以外的其他发信地址生效。
2. 关闭发信地址级异步通知开关,则发信地址级和账户级异步通知对该发信地址都不生效。
3. 删除发信地址异步通知,则账号级设置对该发信地址生效。
拉取消息监听的代码可以参考短信回执的sdk-demo:https://help.aliyun.com/document_detail/101874.html
在编辑代码的时候有个问题需要注意一下:MNS消息接收的sdk中有两个jar包不能直接从maven中引入,需要自己手动处理一下,上传至自己maven仓库或者直接手动引入,具体的jar在demo中的lib中可以找到。需要注意一下,发送短信用的是:dysmsapi,MNS使用的是:dybaseapi。
alicom-mns-receive-sdk-1.0.1.jar
aliyun-java-sdk-dybaseapi-1.0.0.jar
因为只使用了接收消息回执,重写了demo中的DefaultAlicomMessagePuller类,线程池使用默认配置,其他根据自己实际情况配置
public void startEmailReciptListener(){ logger.info("<<<<<<<<<<<<<<<<<<<<<<<<<<<<startEmailReciptListener:邮件消息回执监听启动>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); EmailMessagePuller puller=new EmailMessagePuller(); puller.setDebugLogOpen(emailOptionsProperties.getDebugLogOpen()); String accessKeyId = "accessKeyId";//此处需要替换成开发者自己的AK信息 String accessKeySecret="accessKeySecret";//此处需要替换成开发者自己的AK信息 String mnsEndpoint="mnsEndpoint";//指定与ECS相同的地域 String emailQueueName = "emailQueueName";//MNS平台上创建的队列名称 puller.startReceiveMsg(accessKeyId,accessKeySecret,mnsEndpoint,emailQueueName,new EmailReceiptListener()); }
这里和短信回执有一点不同是,短信的队列是开启配置之后自动创建的,短信在实例化连接队列的时候需要向阿里进行请求认证,邮件的直接实例化获取队列就可以。
CloudAccount account = new CloudAccount(accessKeyId, accessKeySecret, mnsEndpoint);
MNSClient client = account.getMNSClient();
CloudQueue queue = client.getQueueRef(emailQueueName);
因为具体哪个环境的消息需要自己在业务层面中进行判断处理,之前针对不同环境分别创建了不同的发信地址,所有根据回执消息中的from字段进行区分(投递事件回执消息中account、from字段都是发件人地址,返回的信息都相同,没有区别,但是account只有投递事件中有)。这时候有一点需要注意:处理完消息后,是否将队列中的消息删除是根据处理结果返回的true/false进行判断的,如果返回false,消息会重新进入队列。所以我首先判断是否是当前环境消息,非本环境消息直接返回false,重新进入队列。(这里有一个问题,就是这条消息可能一直没被对应的环境拉取到,只能自行在代码层面进行幂等性处理。需要后期优化)。
static class EmailReceiptListener implements EmailMessageListener { @Override public boolean dealMailMessage(Message message) { // logger.info("<<<<<<<<<<<<<EmailReceiptListener:邮件消息回执messageId:"+message.getMessageId()+",处理开始:"+DateUtil.getCurrDate()+">>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); try{ Map dealMap = dealMsgStr(message.getMessageBodyAsRawString());//自行处理回执消息,以”&”分隔key-val对,key-val对用”=”分隔 //判断是否是当前环境消息 if(dealMap.get("from").equals(emailAccount)){ switch (dealMap.get("event").toString()) { case "deliver": /** * 回执消息中,err_code、err_msg为对方的收信服务器返回信息,不受阿里云控制 * failed_type为阿里云返回信息,枚举说明:https://help.aliyun.com/document_detail/52048.html? * 投递消息只触发一次 */ break; case "Click": /** * 点击事件会触发多次,如果邮件中有多个连接,或者相同连接点击多次,会多次发送回执消息,定时整点的时候统一推送 */ break; case "Open": /** * 打开事件会触发多次,如果邮件打开多次,会多次发送回执消息,定时整点的时候统一推送 */ break; default: logger.error(dealMap.get("event").toString() + "=========事件不存在"); } // logger.info(message.getMessageBodyAsRawString()+"====="+",处理结束:"+DateUtil.getCurrDate()); }else{ // logger.info("<<<<<<<<<<<<<EmailReceiptListener:非当前环境消息回执:" + dealMap.get("from")+",处理结束:"+DateUtil.getCurrDate()+">>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); return false; } } catch (Throwable e) { logger.error("处理邮件消息回执失败:"+message.getMessageBodyAsString()+e); return false; } //消息处理成功,返回true, SDK将调用MNS的delete方法将消息从队列中删除掉 return true; } }
到这一步就成功接收到消息回执了,可以进行记录、重发等后续业务处理了。
仅以此记录成长,若是能提供帮助,再好不过了。