文章目录
- 前言
- 一、设计目标
- 二、设计实现
- 1. 开发框架
- 2. 配置管理后台
- 3. 屏蔽渠道差异
- 4. 各阶段工作内容
- 4.1 业务处理前期准备阶段
- 4.2 业务处理阶段
- 4.2.1 交易处理模板获取
- 4.2.2 参数验证
- 4.2.3 幂等性验证
- 4.2.4 交易数据准备服务获取
- 4.2.5 路由处理
- 4.2.6 支付渠道数据补全
- 4.2.7 交易信息入库
- 4.2.8 业务处理
- 4.2.9 后置处理
- 4.3 业务处理后期响应阶段
- 4.3.1 请求响应
- 4.3.2 渲染视图
- 总结
前言
从《支付系统设计三:渠道网关设计01-总览》—>《支付系统设计三:渠道网关设计08-结果响应》几篇文章主要讲解了支付网关系统为了实现在线对接支付渠道
的实现脉络。
一、设计目标
为了增加复用、缩短业务的落地时间,就需要很多通用的能力、产品。在我们的交付过程中,主要有两个层次:
基础能力:相对原子的能力是基础(域)能力,这个可以较好地支持业务定制。由于比较基础,表达的产品能力范围也是很大的。
平台产品:基础能力的通用性,意味着缺少对场景的理解,缺少了进一步提升生产效率的“基因”。所以在交付的时候,会基于一些高频场景进行抽象,形成平台的产品能力,争取做到“拆箱即用”。业务基于“平台产品”这层进行定制的时候,理解成本会大大减少。
payGw系统作为基础能力系统:
- 1.统一支付出口,提供丰富的支付工具原子能力(代扣、批扣、代付、批付、快捷支付、网关支付、鉴权、银行卡签约等);
- 2.与业务场景解耦,业务场景的多变特点不会体现在Paygw系统中 ;
- 3.屏蔽各支付渠道的接入差异(通讯方式差异、加密方式差异、业务报文差异);
- 4.快速接入支付渠道的能力(可以达到不停机接入);
如下为了给各业务线提供代扣业务能力,所以支付平台对接了不同的支付渠道的代扣产品,对于大型的支付系统可能对接了几十家支付渠道。
当我们将支付系统做成一个产品提供给没有自研能力的但是需要支付环节的公司时候,如何满足甲方公司对系统稳定性、快速接入支付渠道的需求?同时降低乙方建设成本?答案是:建立样板工程,将项目产品化
,然后拿着样板工程去寻目标客户,符合度越高,后期工程实施成本越低,稍微变动即可满足客户需求,同时具有高扩展性,对于客户定制化需求也能轻松满足。
paygw系统作为支付基础能力提供的系统,通过对接不同的支付渠道为支付产品层(paycore系统)提供支付工具原子能力,由于不同的支付渠道所需要的请求参数,报文格式,通讯协议等个不相同,那么如何能屏蔽这些不同,快速接入支付渠道?对接支付渠道时不需要进行系统上线,不用披着北京凌晨的月光回家,只需要简单配置就能完成系统对接工作,白天也可以进行系统上线,带着这个设计目标开始我们的整个流程设计。
本系列博文,就国内支付系统特性进行分析总结,目标设计开发一套具有高可复用性的支付系统,可以轻松满足大多数公司的支付功能需求。
二、设计实现
1. 开发框架
在《支付系统设计二:统一开发框架》的基础上,进行paygw子系统具体代码编写。
2. 配置管理后台
在对接新支付渠道时,后台管理系统需要进行对应的配置,因为以不停机作为系统设计目标,所以最大化参数配置,将可变的参数进行系统话配置,最大限度的满足不同支付渠道的需要。
3. 屏蔽渠道差异
分别从以下四个阶段介绍如何屏蔽各个支付渠道的差异:接收客户端请求阶段、服务端消息发送阶段、接收服务端返回报文、客户端请求返回阶段。
对于如何屏蔽不同支付渠道的差异,主要是将通讯相关内容进行配置化,并使用脚本Groovy和模板引擎Velocity分别进行解析、组装工作,总之思想就是将可变的参数进行配置化。
4. 各阶段工作内容
根据不同维度,我们可以把这个流程拆分多个环节,下面我们将开篇12个处理过程按照普通开发人员的程序编码思想粗略的拆解为三个阶段。业务处理阶段、业务处理前期准备阶段、业务处理后期响应阶段。这三层阶段划分比较符合我们日常的逻辑处理思想,比如写接收支付渠道交易结果接口,是不是先将渠道报文转化为系统统一对象,处理业务,构建响应对象。
下面我们从这个角度再进行总结下,希望感兴趣的同学能够知道我在说什么。
4.1 业务处理前期准备阶段
业务处理前期准备阶段如提供的交易结果回调通知接口将支付渠道的请求报文转换为系统对象的过程,同样的在我们写结果回调通知接口的时候我们需要看渠道接口文档,确定其使用的协议类型(http/https/hessian等)、报文格式(json/xml/byte等)、编码(UTF-8/GBK/GB2312等)等才能知道怎么转换为系统所需的对象。
同样的,在我们这个系统设计中,只是将这些作为可配置化,根据对应的协议类型获取到对应的协议处理器进行处理,然后根据报文格式以及编码进行数据读取,然后再根据对应的解析脚本将读取到的数据转化为系统所需的指定对象。
4.2 业务处理阶段
在业务处理阶段主要是通过全局上下文中的MessageDescription属性贯穿整个流程处理:
交易管理类,可以根据transCode找到对应的映射处理。
/**
* @author Kkk
* @Describe: 交易管理
*/
public class TransactionManager {
/**
* 交易-模板映射
*/
private static final Map<TransactionEnum, String> templateMapping = new HashMap<TransactionEnum, String>();
/**
* 交易-模型映射
*/
private static final Map<TransactionEnum, Class<? extends AggregateBase>> demainMapping = new HashMap<TransactionEnum, Class<? extends AggregateBase>>();
/**
* 交易-查询交易映射
*/
private static final Map<TransactionEnum, TransactionEnum> queryTransactionMapping = new HashMap<TransactionEnum, TransactionEnum>();
/**
* 交易-流水序列类型
*/
private static final Map<TransactionEnum, SeqTypeEnum> seqTypeMapping = new HashMap<TransactionEnum, SeqTypeEnum>();
/**
* 交易-交易码前缀映射
*/
private static final Map<TransactionEnum, String> transCodeMapping = new HashMap<>();
static {
initTemplateMapping();
initDemainMapping();
initQueryTransactionMapping();
initSeqTypeMapping();
initTransCodeMapping();
}
/**
* 初始化交易-模板映射
*/
private static void initTemplateMapping() {
templateMapping.put(TransactionEnum.WAP_SIGN, "sigleTransTemplate");
templateMapping.put(TransactionEnum.WAP_SIGN_QRY, "queryTransTemplate");
templateMapping.put(TransactionEnum.QUICK_PAY, "sigleTransTemplate");
templateMapping.put(TransactionEnum.QUICK_PAY_SEND_MSG, "sigleTransTemplate");
templateMapping.put(TransactionEnum.QUICK_PAY_CONFIRM, "sigleConfirmTemplate");
templateMapping.put(TransactionEnum.QUICK_PAY_QRY, "queryTransTemplate");
templateMapping.put(TransactionEnum.WAP_PAY, "sigleTransTemplate");
templateMapping.put(TransactionEnum.WAP_PAY_QRY, "queryTransTemplate");
templateMapping.put(TransactionEnum.DEDUCT, "sigleTransTemplate");
templateMapping.put(TransactionEnum.DEDUCT_ASY, "sigleTransTemplate");
templateMapping.put(TransactionEnum.DEDUCT_QRY, "queryTransTemplate");
templateMapping.put(TransactionEnum.DEPUTE, "sigleTransTemplate");
templateMapping.put(TransactionEnum.DEPUTE_ASY, "sigleTransTemplate");
templateMapping.put(TransactionEnum.DEPUTE_QRY, "queryTransTemplate");
templateMapping.put(TransactionEnum.BATCH_DEDUCT, "batchTransTemplate");
templateMapping.put(TransactionEnum.BATCH_DEDUCT_QRY, "queryTransTemplate");
templateMapping.put(TransactionEnum.BATCH_DEPUTE, "batchTransTemplate");
// ... ...
}
/**
* 初始化交易-交易码前缀映射
*/
private static void initTransCodeMapping() {
transCodeMapping.put(TransactionEnum.WAP_PAY, "wapPay");
transCodeMapping.put(TransactionEnum.WAP_PAY_QRY, "wapPay");
transCodeMapping.put(TransactionEnum.DEDUCT_ALLOT_ACCT, "deduct");
transCodeMapping.put(TransactionEnum.DEDUCT_ALLOT_ACCT_QRY, "deduct");
// ... ...
}
/**
* 初始化交易-模型映射
*/
private static void initDemainMapping() {
demainMapping.put(TransactionEnum.WAP_PAY, WapPay.class);
demainMapping.put(TransactionEnum.WAP_PAY_QRY, WapPay.class);
demainMapping.put(TransactionEnum.DEDUCT, Deduct.class);
demainMapping.put(TransactionEnum.DEDUCT_ASY, Deduct.class);
//... ...
}
/**
* 初始化交易-查询交易映射
*/
private static void initQueryTransactionMapping() {
queryTransactionMapping.put(TransactionEnum.WAP_PAY, TransactionEnum.WAP_PAY_QRY);
queryTransactionMapping.put(TransactionEnum.DEDUCT, TransactionEnum.DEDUCT_QRY);
queryTransactionMapping.put(TransactionEnum.DEDUCT, TransactionEnum.DEDUCT_QRY);
queryTransactionMapping.put(TransactionEnum.DEDUCT_ASY, TransactionEnum.DEDUCT_QRY);
// ... ...
}
/**
* 初始化交易-流水序列映射
*/
private static void initSeqTypeMapping() {
seqTypeMapping.put(TransactionEnum.AUTH, SeqTypeEnum.SEQ_AUTH);
seqTypeMapping.put(TransactionEnum.AUTH_SEND_MSG, SeqTypeEnum.SEQ_SMS);
seqTypeMapping.put(TransactionEnum.AUTH_CONFIRM, SeqTypeEnum.SEQ_AUTH);
//... ...
}
/**
* 根据交易码获取模板
* @param transCode
* @return
*/
public static String getTemplate(String transCode) {
TransactionEnum transaction = TransactionEnum.getByCode(transCode);
return templateMapping.get(transaction);
}
/**
* 根据交易码获取映射
* @param transCode
* @return
*/
public static String getTransCode(String transCode) {
TransactionEnum transaction = TransactionEnum.getByCode(transCode);
return transCodeMapping.get(transaction);
}
/**
* 根据交易码获取模型类
* @param transCode
* @return
*/
public static Class<? extends AggregateBase> getDomainClass(String transCode) {
TransactionEnum transaction = TransactionEnum.getByCode(transCode);
return demainMapping.get(transaction);
}
/**
* 根据交易码获取查询交易码
* @param transCode
* @return
*/
public static String getQueryTransCode(String transCode) {
TransactionEnum transaction = TransactionEnum.getByCode(transCode);
return queryTransactionMapping.get(transaction).getCode();
}
/**
* 根据交易码获取流水序列
* @param transCode
* @return
*/
public static SeqTypeEnum getSeqType(String transCode) {
TransactionEnum transaction = TransactionEnum.getByCode(transCode);
SeqTypeEnum se = seqTypeMapping.get(transaction);
if (se == null) se = SeqTypeEnum.SEQ_COMM;
return se;
}
}
4.2.1 交易处理模板获取
在进行业务处理第一步先根据客户端交易码从交易-模板映射Map中获取到对应的交易处理模板。
private static final Map<TransactionEnum, String> templateMapping = new HashMap<TransactionEnum, String>();
如Wap支付对应的 sigleTransTemplate:
templateMapping.put(TransactionEnum.WAP_PAY, "sigleTransTemplate");
4.2.2 参数验证
由于我们在使用解析脚本进行参数转化时候将系统调用上送参数存储到MessageDescription中的datas(Map)中,所以我们得想办法校验对应的交易参数是否合法,我们采用Yml配置化方式进行参数校验。
根据transCode获取对应的校验器集合进行参数校验:
/**
* 参数校验
* @param transCode
* @param data
*/
public void validate(String transCode, Map<String, Object> data) {
Map<String, List<FieldPolicy>> fieldPolicyListMap = this.fieldValidateYamlProcessor.getFieldPolicyListMap();
if (!CollectionUtils.isEmpty(fieldPolicyListMap)) {
List<FieldPolicy> fieldPolicyList = fieldPolicyListMap.get(transCode);
if (!CollectionUtils.isEmpty(fieldPolicyList)) {
for (FieldPolicy fieldPolicy : fieldPolicyList) {
fieldPolicy.validate(StringUtils.valueOf(data.get(fieldPolicy.getField())));
}
}
}
}
每一种交易类型配置完成一次就行了,此处不再展开了。
4.2.3 幂等性验证
使用Redis进行幂等校验,比较简单,不再展开了。
4.2.4 交易数据准备服务获取
通过约定的格式名称进行交易数据准备服务的获取:
/**
* 准备数据service bean 后缀
*/
private final static String PREPAREDATASERVICE_SUFF = "PrepareDataServiceImpl";
/**
* 数据补全服务缓存
*/
@Autowired
private Map<String, PrepareDataService> prepareDataServiceMap;
/**
* 获取数据补全服务
*/
public PrepareDataService getPrepareDataService(String transCode) {
String key = new StringBuilder(transCode).append(PREPAREDATASERVICE_SUFF).toString();
PrepareDataService prepareDataService = prepareDataServiceMap.get(key);
if (prepareDataService == null) {
LoggerUtil.info(logger, "交易({})-未获取到准备交易数据服务-采用默认服务(defaultPrepareDataService)", transCode);
prepareDataService = defaultPrepareDataService;
}else {
LoggerUtil.info(logger, "交易({})-获取到准备交易数据服务({})", transCode, key);
}
return prepareDataService;
}
因为不同的交易类型需要补全的信息也不同,所以通过transCode+PrepareDataServiceImpl获取对应的补全服务类。
在交易数据准备服务类中主要是补全一些数据表需要的字段,如交易表中的业务处理状态、交易订单状态、开户机构信息(分行号、联行号等)、省市信息等。
4.2.5 路由处理
调用支付路由系统获取对应的支付渠道以及账户号,支付路由相关设计实现见《支付路由系统设计一:实现效果展示》系列文章。
将获取到的支付渠道信息填充到MessageDescription中。
4.2.6 支付渠道数据补全
/**
* 准备数据service bean 后缀
*/
private final static String COMPLETIONDATASERVICE_SUFF = "CompletionDataServiceImpl";
/**
* 数据补全服务
*/
@Autowired
private Map<String, CompletionDataService> completionDataServiceMap;
/**
* @Description 获取数据补全服务
* @Params
* @Return
* @Exceptions
*/
public CompletionDataService getCompletionDataService(String transCode) {
String key = new StringBuilder(transCode).append(COMPLETIONDATASERVICE_SUFF).toString();
CompletionDataService completionDataService = completionDataServiceMap.get(key);
if (completionDataService == null) {
LoggerUtil.info(logger, "交易({})-未获取到交易数据补全服务-采用默认服务", transCode);
completionDataService = defaultCompletionDataService;
}
return completionDataService;
}
因为不同的交易类型需要补全的信息也不同,所以通过transCode+CompletionDataServiceImpl获取对应的补全服务类进行数据补全。在补全渠道信息时,对于特定渠道所需属性通过补全脚本进行补全。
主要补全内容:根据支付路由系统输出的账号,对账号关联的数据填充到上下文中,并将账号关联的商户号以及对应补充的属性信息填充到上下文中,使用对应脚本进行特定属性的补全。
4.2.7 交易信息入库
transCode+DomainDBSaveServiceImpl获取对应的数据持久化服务
/**
* 准备数据service bean 后缀
*/
private final static String DOMAINDBSAVESERVICE_SUFF = "DomainDBSaveServiceImpl";
/**
* 领域模型持久化服务
*/
@Autowired
private Map<String, DomainDBSaveService> domainDBSaveServiceMap;
/**
* 获取数据持久化服务
* @param transCode
* @return
*/
public DomainDBSaveService getDomainDBSaveService(String transCode) {
String dbTransCode = TransactionManager.getTransCode(transCode);
StringBuilder key = new StringBuilder();
if (StringUtils.isNotBlank(dbTransCode)) {
key.append(dbTransCode).append(DOMAINDBSAVESERVICE_SUFF);
} else {
key.append(transCode).append(DOMAINDBSAVESERVICE_SUFF);
}
DomainDBSaveService domainDBSaveService = domainDBSaveServiceMap.get(key.toString());
if (domainDBSaveService == null) {
LoggerUtil.info(logger, "交易({})-未获取到交易入库服务-采用默认服务", transCode);
domainDBSaveService = defaultDomainDBSaveService;
}
return domainDBSaveService;
}
在数据持久化服务中根据交易码获取模型类
/**
* 交易-模型映射
*/
private static final Map<TransactionEnum, Class<? extends AggregateBase>> demainMapping = new HashMap<TransactionEnum, Class<? extends AggregateBase>>();
根据模型类+指定指定后缀获取到模型仓储进行数据持久化操作
/**
* @Description 获取域模型仓储
*/
public BusinessModelRepository getBusinessModelRepository(Class domainClass) {
String domainClassName = StringUtils.convertFirstCharToLower(domainClass.getSimpleName());
String beanName = "";
if (domainClassName.endsWith(DOMAIN_SUFF) || domainClassName.endsWith(ENTITY_SUFF)) {
beanName = domainClassName + DOMAIN_REPOSITORY_SIMPLE_SUFF;
} else {
beanName = domainClassName + DOMAIN_REPOSITORY_SUFF;
}
return ApplicationContextUtil.getBean(beanName, BusinessModelRepository.class);
}
4.2.8 业务处理
transCode+BusinessServiceImpl获取对应的业务处理服务
/**
* 业务service bean 后缀
*/
private final static String BUSINESSSERVICE_SUFF = "BusinessServiceImpl";
@Autowired
private Map<String, BusinessService> businessServiceMap;
/**
* 获取业务服务Service
* @param transCode+BusinessServiceImpl
* @return
*/
public BusinessService getBusinessService(String transCode) {
String key = new StringBuilder(transCode).append(BUSINESSSERVICE_SUFF).toString();
BusinessService businessService = businessServiceMap.get(key);
return businessService;
}
主要进行如下操作:
- 报文组装(报文格式化+加签加密)
- 报文发送
- 报文解析(报文数据解析+验签解密)
- 响应码解析
4.2.9 后置处理
- 交易数据更新
同数据持久化一样,先从领域模型更新服务工厂中获取到数据库更新服务,然后获取领域模型,根据领域模型获取领域模型仓储,进行交易数据更新。 - 异步通知
对于一些异步交易发送交易结果通知。
4.3 业务处理后期响应阶段
4.3.1 请求响应
根据报文组装引擎将MessageDescription中的datas属性填充到对应的模板中以响应客户端请求。
4.3.2 渲染视图
/**
* 渲染视图
* @param transCode
* @param response
* @param messageEnvelope
*/
public void render(String transCode, HttpServletResponse response, MessageEnvelope messageEnvelope) {
MessageFormatEnum messageFormat = messageEnvelope.getMessageFormat();
String contentType = StringUtils.isBlank(messageFormat.getMessage()) ? MediaType.APPLICATION_JSON.toString() : messageFormat.getMessage();
try {
response.setContentType(contentType);//考虑json和html两种方式
response.setCharacterEncoding(messageEnvelope.getEncode().getMessage());
response.getWriter().write((String) messageEnvelope.getContent());
} catch (IOException e) {
LoggerUtil.error(logger, "交易({})-同步响应结果-异常", transCode, e);
}
}
总结
本篇对支付网关的实现大致过程进行总结了下,主要是高度配置化、解析脚本、模板引擎的使用。