§. 短信平台(聚合系统)的回调-业务说明
我司短信平台聚合了朗宇、漫道、华信等多家短信服务商通道,并输出统一的接口能力供业务系统使用。
本文以短信平台(sms)为例。来说一下各短信通道回调sms的代码实现。
注:下文提到的”短信服务商“、”短信通道“、”通道“表示相同概念。
接收到短信发送结果的回调通知,我们sms做的事情包括:
- 从request对象里获取到请求报文
- 数据安全处理,如验签/解密
- 解析请求报文,获取消息id、发送状态等数据属性
- 根据`消息id`判断是否在系统库里存在发送记录
- 将通道侧发送状态转换为系统内的短信发送状态
- 持久保存通知结果数据
- 持久更新短信发送记录的发送状态
- 其他相关业务处理(例如:如果通道返回“黑名单”、“风控拦截”等特殊情况,触发系统告警)
- 回写消息,响应给短信服务商。
现在sms系统为每个短信通道提供了不同的回调接口,对应各自的处理方法。如
短信通道 |
接收回调通知的API |
|
朗宇 | com.sms.restapi.LangyuSmsNotify#onNotify
|
|
漫道 | com.sms.restapi.MDSmsNotify#onNotify
|
|
华信 | com.sms.restapi.HuaXinSmsNotify#onNotify
|
§. 避免烟囱式代码堆砌
如果在每个 onNotify 方法里都编写上面的实现代码,显然,这就是在重复竖烟囱。未来对接新的短信通道时,如果沿用这种实现方式,必然就又会多出一个个的烟囱。
§. 那么,当如何进行程序设计呢?
我们来看看下面的good-practice。
先分析上面处理回调的那9个步骤。我们看哪些是通道独有的,哪些是可以共用的。
step |
程序逻辑 |
通道独有逻辑 |
公共逻辑 |
1 | 从request对象里获取到请求报文 | ✅ | |
2 | 数据安全处理,如验签/解密 | ✅ | |
3 | 解析请求报文,获取消息id、发送状态等数据属性 | ✅ | |
4 | 根据`消息id`判断是否在系统库里存在发送记录 | ✅ | |
5 | 将通道侧发送状态转换为系统内的短信发送状态 | ✅ | |
6 | 持久保存通知结果数据 | ✅ | |
7 | 持久更新短信发送记录的发送状态 | ✅ | |
8 | 其他相关业务处理(例如:如果通道返回“黑名单”、“风控拦截”等特殊情况,触发系统告警) | ✅ | |
9 | 回写消息,响应给短信服务商。 | ✅ |
前5步和第9步是通道独有的,中间6、7、8三步是公共的。为了方便阅读,下文把这2个集合表示为 “{1~5,9}” 和“{6,7,8}”。
首先,我们先将 {6,7,8} 这步封装起来实现复用。
我们业务逻辑层新建SmsService#handSmsNotify 方法,职责是处理短信通道的回调结果,包括 {6,7,8} 步中的数据持久化和其他业务处理。
public boolean handSmsNotify(***){ 6 7 8 }
这样一来,各个通道处理回调的接口逻辑简化为:
"/SmsReport/***") (public void onNotify(HttpServletRequest){ 1 2 3 4 5 smsService.handSmsNotify; 9 }
接下来,我们说说 {1~5,9}步,这些通道独有的逻辑代码,应该在Web控制器层吗?
非也!
按照系统模块职责,这些通道独有的逻辑,应该放置在通道层,并提供api给Web控制层来调用。
这样的话,怎么来组织我们的代码呢?
达芬奇密码就是面向接口编程。抽象出来公共的接口行为,基于OOP的多态性,由具体的通道实现类封装各自的不同点。这些通道的不同点包括从request获取数据的方式、数据安全校验、数据的解析,等等。
SMS程序中,通道类实现的interface是SmsSDK,我们在这个interface里新建一个接口方法:handleNotify。方法签名为:
|
方法返参`SupplierNotifyResultVO`的结构是什么呢?这是程序设计上的一个重点。`SupplierNotifyResultVO`包括 是否成功受理、回写的消息内容、通道侧消息msgId、通道侧发送状态码、通道侧发送状态描述、对应的sms平台的发送状态。
/** * 短信发送结果通知,通道层解析数据后使用这个模型 */ public class SupplierNotifyResultVO { /** * 是否受理成功。成功才有{@link #reportList} */ private boolean isSuccess; /** * 受理失败时的错误描述 */ private String errorMsg; /** * 业务执行完成后需要回写的消息内容 */ private String writeBackText = ""; /** * 通知给我方的发送结果数据(每次回调会通知1条或多条发送结果,所以使用集合) */ private List<Report> reportList; public static class Report { private SmsStatusEnum sysSmsStatus; // 对应的sms平台的发送状态 private String msgId; // 通道侧消息msgId private String statusCode; // 通道侧发送状态码 private String statusDesc; // 通道侧发送状态描述 private String mobile; // 接收短信的用户手机号 private Date receiveTime; // 用户手机收到短信的时间 } public static SupplierNotifyResultVO success(List<Report> reportList, String writeBackText) { ... } public static SupplierNotifyResultVO success(Report report, String writeBackText) { ... } public static SupplierNotifyResultVO error(String errorMsg) { ... } }
这样一来,各个通道处理回调的接口逻辑进一步简化为:
"/SmsReport/***") (public void onNotify(HttpServletRequest){ SmsSDK smsSDK = SmsSDKFactory.get(**); SupplierNotifyResultVO supplierNotifyResultVO = smsSDK.handleNotify(request); smsService.handSmsNotify(SupplierNotifyResultVO); 回写 supplierNotifyResultVO.responseMsg; }
附华信通道实现类的回调代码:
public SupplierNotifyResultVO handleNotify(HttpServletRequest request) { String xml = IoUtil.read(request.getInputStream(), StandardCharsets.UTF_8); if (StringUtils.isBlank(xml)) { log.error("华信短信通知报文为空"); return SupplierNotifyResultVO.error("no data"); } log.info("华信的推送报告报文={}", xml); HuaxinSmsNotifyModel smsReturn = JAXBXmlUtils.parseXml(xml, HuaxinSmsNotifyModel.class); if (null == smsReturn || CollectionUtil.isEmpty(smsReturn.getReports())) { log.info("华信 解析回执为空"); return SupplierNotifyResultVO.error("no data"); } List<HuaxinSmsNotifyModel.HuaxinSmsReport> reports = smsReturn.getReports(); List<SupplierNotifyResultVO.Report> reportList = new ArrayList<>(); for (HuaxinSmsNotifyModel.HuaxinSmsReport reportModel : reports) { SupplierNotifyResultVO.Report report = new SupplierNotifyResultVO.Report(); report.setMobile(StringUtils.defaultString(reportModel.getMobile()).trim()); report.setMsgId(StringUtils.defaultString(reportModel.getTaskId()).trim()); report.setStatusCode(StringUtils.defaultString(reportModel.getErrorCode()).trim()); report.setReceiveTime(DateUtil.parseDateTime(reportModel.getReceiveTime())); report.setStatusDesc(ReportCodeMapper.getDesc(report.getStatusCode())); report.setSysSmsStatus(getStatus(report.getStatusCode())); reportList.add(report); } return SupplierNotifyResultVO.success(reportList, "1"); }
至此,我们的程序设计已经比较完美了。各个通道在处理短信回调时,Web控制层、业务层、通道层各司其职,也没有了烟囱式的代码堆砌。程序扩展方面,当需要对接新通道时,开发者只需要关注Web控制层和通道层,其中Web控制层的开发工作是提供简单的restapi接口,通道层的开发工作则是通道对接相关的代码。
尽管如此,我们有必要再提起一个事情。那就是,Web控制层为各通道暴露的回调接口。
我们不难发现,各个通道的回调入口的逻辑是相似的。
so,我们把这些回调接口统一成一个,岂不更香!
这时,需要注意的是就是程序怎么识别出来特定的通道标识。easy~ SpringMVC为我们提供了 @PathVariable 注解。
"/SmsReport/{supplierCode}") (public void onNotify( ("supplierCode") String supplierCode, HttpServletRequest){ assert StringUtils.notBlank(supplierCode); SmsSupplierEnum supplier = SmsSupplierEnum.valueOf(supplierCode); assert supplier != null; SmsSDK smsSDK = SmsSDKFactory.get(supplier); SupplierNotifyResultVO SupplierNotifyResultVO = smsSDK.handleNotify(request); smsService.handSmsNotify(SupplierNotifyResultVO); 回写 SupplierNotifyResultVO.responseMsg; }
§. 附:sms项目分层结构
com.sms | ||||
.restapi | ||||
.SmsSendController | 为公司内业务系统提供的短信发送聚合接口 | |||
.SupplierNotifyController | 暴露给短信服务商的回调接口(*本文用到) | |||
... | ||||
.bizservice | ||||
.SmsService | 短信发送业务服务类 | |||
#sendSms | 发送短信 | |||
#handSmsNotify | 处理短信发送通知结果 | |||
... | ||||
modules | 单表操作的Manager及DAO | |||
... | ||||
.supplier | ||||
.vo | POJO模型类 | |||
.SupplierNotifyResultVO | 短信发送结果通知,通道层解析数据后使用这个模型 (*本文用到) |
|||
.SmsSDK | interface -短信服务商接口能力 | |||
#sendSms | 调用短信服务商API,并解析响应报文 | |||
#handleNotify | 解析短信回调报文(*本文用到) |
|||
.huaxin | 华信服务商对接package | |||
.HuaXinSmsSDK | 华信短信接口能力 implemented SmsSDK |
|||
.langyu | 朗宇服务商对接package | |||
.LangYuSmsSDK | 朗宇短信接口能力 implemented SmsSDK |
|||
... |