首页 > 编程语言 >【优秀程序设计】【good-practice】聚合系统如何实现通道侧回调的业务结果通知?

【优秀程序设计】【good-practice】聚合系统如何实现通道侧回调的业务结果通知?

时间:2024-09-07 23:26:25浏览次数:8  
标签:good 短信 practice 发送 SupplierNotifyResultVO 程序设计 回调 public 通道

§. 短信平台(聚合系统)的回调-业务说明

我司短信平台聚合了朗宇、漫道、华信等多家短信服务商通道,并输出统一的接口能力供业务系统使用。

本文以短信平台(sms)为例。来说一下各短信通道回调sms的代码实现。

注:下文提到的”短信服务商“、”短信通道“、”通道“表示相同概念。

image

 

 

接收到短信发送结果的回调通知,我们sms做的事情包括:

  1. 从request对象里获取到请求报文
  2. 数据安全处理,如验签/解密
  3. 解析请求报文,获取消息id、发送状态等数据属性
  4. 根据`消息id`判断是否在系统库里存在发送记录
  5. 将通道侧发送状态转换为系统内的短信发送状态
  6. 持久保存通知结果数据
  7. 持久更新短信发送记录的发送状态
  8. 其他相关业务处理(例如:如果通道返回“黑名单”、“风控拦截”等特殊情况,触发系统告警)
  9. 回写消息,响应给短信服务商。

 

现在sms系统为每个短信通道提供了不同的回调接口,对应各自的处理方法。如

短信通道

接收回调通知的API

朗宇

com.sms.restapi.LangyuSmsNotify#onNotify

 

 

 

漫道

com.sms.restapi.MDSmsNotify#onNotify

 

 

 

华信

com.sms.restapi.HuaXinSmsNotify#onNotify

 

 

 

@RequestMapping("/SmsReport/LangyuSmsReport")

public void onNotify(HttpServletRequest)

@RequestMapping("/SmsReport/MDSmsReport")

public void onNotify(HttpServletRequest)

@RequestMapping("/SmsReport/Huaxin")

public void onNotify(HttpServletRequest)

 

§. 避免烟囱式代码堆砌

如果在每个 onNotify 方法里都编写上面的实现代码,显然,这就是在重复竖烟囱。未来对接新的短信通道时,如果沿用这种实现方式,必然就又会多出一个个的烟囱。

image.png

 

§. 那么,当如何进行程序设计呢?

 

我们来看看下面的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
}

这样一来,各个通道处理回调的接口逻辑简化为:

@RequestMapping("/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 handleNotify(HttpServletRequest);

方法返参`SupplierNotifyResultVO`的结构是什么呢?这是程序设计上的一个重点。`SupplierNotifyResultVO`包括 是否成功受理、回写的消息内容、通道侧消息msgId、通道侧发送状态码、通道侧发送状态描述、对应的sms平台的发送状态。

/**
 * 短信发送结果通知,通道层解析数据后使用这个模型
 */
@Getter public class SupplierNotifyResultVO {
    /**
     * 是否受理成功。成功才有{@link #reportList}
     */
    private boolean isSuccess;
    /**
     * 受理失败时的错误描述
     */
    private String errorMsg;
    /**
     * 业务执行完成后需要回写的消息内容
     */
    private String writeBackText = "";
    /**
     * 通知给我方的发送结果数据(每次回调会通知1条或多条发送结果,所以使用集合)
     */
    private List<Report> reportList;
 
 
    @Data 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) { ... }
}

 

这样一来,各个通道处理回调的接口逻辑进一步简化为:

@RequestMapping("/SmsReport/***")
public void onNotify(HttpServletRequest){
    SmsSDK smsSDK = SmsSDKFactory.get(**);
    SupplierNotifyResultVO supplierNotifyResultVO = smsSDK.handleNotify(request);
     
    smsService.handSmsNotify(SupplierNotifyResultVO);
     
    回写 supplierNotifyResultVO.responseMsg;
}

 

附华信通道实现类的回调代码:

@Override
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 注解。

 

 

 

§. 附: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

    ...    

 

 

标签:good,短信,practice,发送,SupplierNotifyResultVO,程序设计,回调,public,通道
From: https://www.cnblogs.com/buguge/p/18402319

相关文章

  • 程序设计思路-外部请求,中断测试,结算费用,带出logs参数
    1.合同到期,数据也落库,存入到无效数据表中。正常的业务场景。但是如果考虑到合同过期后,没有按时的续签,那么这段时间段内的数据,实际上是需要恢复的。原则:外部第三方请求参数,返回获取的数据,都需要保持入库,有的数据需要考虑恢复和排查问题。2.计算费用接口/方法体,考虑有值的情况......
  • 自习室预约的微信小程序设计与实现-计算机毕业设计源码+LW文档
    摘要随着科技与时代的进步,计算机与手机已成为生活中不可或缺的工具。在高校中,各类管理系统有效地提升了管理效率,但自习室管理却一直滞后,导致学生难以了解座位状态,易引发使用冲突。因此,开发了一款基于微信小程序的陕西省汉台区自习室预约系统,旨在实现座位信息化管理,方便学生查询与......
  • C语言程序设计(初识C语言后部分)
    不要重来,不要白来,不要重来。5.指针和数组数组:一组相同类型元素的集合指针变量:是一个变量,存放的地址要理解数组名大部分情况下是数组的首元素地址6.二级指针先了解一级指针变量二级指针变量(二级指针变量是用来存放一级指针变量的地址的)7.指针数组指针数组是......
  • 面向对象程序设计之链表 list 的简析(C++)
    简介:链表是一个双向的结构,与string与vector不同的是他不支持[]访问,因为链表是由一个节点一个节点连接而成的,并不连续。我们可以在常数量级内对于链表进行插入与删除数据1.构造函数我们在cplusplus.com中可以查到链表总共有四种构造的方式:1.无参构造(默认构造);2.使用n个va......
  • C语言程序设计(初识C语言后部分)
    十九,指针1)指针是什么?指针理解的2个要点:1,指针是内存中一个最小单元的编号,也就是地址2,平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量总结:指针就是地址,口语中说的指针通常指的是指针变量。指针变量:我们可以通过&(取地址操作符)取出变量的内存起始地址,把......
  • C语言程序设计-练习篇
    少年自当扶摇上,揽星衔月逐日光。一,小乐乐走台阶描述:小乐乐上课需要走n阶台阶,因为腿长,所以每次可以选择走一阶或者两阶,那么他一共有多少种走法?输入描述:输入包含一个整数n(1<=n<=30)输出描述:输出一个整数,即小乐乐可以走的方法数#include<stdio.h>//小乐乐走台阶intf......
  • 程序设计—基于网络爬虫的股票价格分析系统 项目源码27486
    摘 要在21世纪信息时代,随着网络技术的飞速发展和大数据时代的到来,股票价格分析对于投资者和金融机构的重要性日益凸显。传统的股票价格分析方法往往依赖于人工收集和整理数据,效率低下且易出错。因此,开发一套基于网络爬虫的股票价格分析系统变得至关重要。本文旨在探讨基于......
  • 程序设计专业的毕业生,要怎么写开题报告呢?开题报告对写论文重要吗?
    好的,我们来聊聊程序设计专业的大学生怎样写开题报告,以及它的重要性:想象一下,开题报告就像是你即将开启的编程项目的预告片。它不需要剧透所有精彩的代码和算法,但得抓住人的眼球,让人明白你要做什么,为什么要做,以及你打算怎么做。“怎么写?” 1.“标题“:就像给你的程序取个响......
  • 0 JavaScript高级程序设计(第4版)【JS红宝书】【详细思维导图】【持续更新】
    ProcessOn访问链接JavaScript高级程序设计(第4版)阅读路线图,涵盖:基本知识进阶内容BOM和DOMJavascriptAPIJavaScript设计模式和实践策略ProcessOn访问链接......
  • 算法专项—第十五届蓝桥杯程序设计题解
    前三道属于签到题目;后面才是有难度的!一、读书一本书共n页,小明计划第一天看x页,此后每一天都要比前一天多看y页,请问小明几天可以看完这本书?输入格式一行输入三个整数n,x,y(20≤n≤5000),(1≤x,y≤20),分别表示书的总页数、计划第一天看的页数以及此后每天都要比前一天多看的页数,整......