xbank示例项目

通过xBank项目来介绍用Biz-SIP业务中台开发一个系统的全流程

xBank项目版本库:https://gitee.com/szhengye/xbank.git

一、概述

xBank是一家商业银行,面向个人客户和公司客户,其中个人客户业务包括存款、贷款、缴费等业务;银行业务渠道除了传统柜面以外,还有网上银行、手机银行、ATM、POS等,最近准备上一个针对银行合作伙伴的基于OPENAPI网关的开放平台渠道。
本示例项目是以个人客户中的存款查询和缴费业务为例子,后台系统对接个人客户存款系统和个人客户信息系统,第三方对接缴费平台,来演示如何打造基于Biz-SIP中间件的银行业务中台。

xBank项目版本库:[https://gitee.com/szhengye/xbank.git]

二、体验Biz-SIP示例项目xbank

1. 示例项目xbank简介

xBank系统的逻辑架构如下:

image.png

按DDD分层架构,应用架构图如下所示:
image.png

3. xbank示例项目模板介绍

打开示例项目,如下图:

image.png

xbank项目分为以下模块:

  • xbank-source:适配层模块,存放外部适配接入的适配层实现。
    • xbank-openapi-source:OPENAPI接入的适配层子模块
  • xbank-app:应用层模块,存放应用层的服务实现。
  • xbank-sink:领域层模块,存放按领域划分的领域服务实现。
    • xbank-account-sink:账户域子模块
    • xbank-customer-sink:客户域子模块
    • xbank-payment1-sink:缴费域子模块(存储转发)
    • xbank-payment1-sink:缴费域子模块(交易补偿)
  • xbank-infrastructure:基础设施层模块,存放对数据库、内容存储、HSM等基础设施的访问能力实现。
    • xbank-account-db:账户数据库访问子模块
    • xbank-customre-db:客户数据库访问子模块
  • xbank-client:接口模块,存放各层之间的服务接口,以及相关数据DTO。
    • xbank-account-sink-client:账户域服务接口子模块
    • xbank-customer-sink-client:客户域服务接口子模块
    • xbank-app-client:应用服务接口子模块

各层之间调用关系如下所示:

## 4. 创建数据库 执行项目中xbank-infrastructure/xbank-sql/xbank.sql脚本,以建立xbank演示库。

5. 启动xbank项目

  • 启动应用层服务整合器
    • 执行xbank-app模块中的XbankAppApplication
  • 启动领域层领域服务
    • 执行xbank-customer-sink子模块中的CustomerSinkApplication
    • 执行xbank-account-sink子模块中的AccountSinkAppliction
  • 启动适配层OPENAPI接入服务
    • 执行xbank-openapi-source子模块中的OpenapiSourceApplication

4. 测试接口

4.1 查询客户信息

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/sink/customer" -X POST --data '{"methodName":"getCustomer","params":["001"]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "appServiceId": "/sink/customer",
  "traceId": "92a7422ffdff4d10942db56108e86048",
  "parentTraceId": null,
  "timestamp": 1640692635697,
  "data": {
    "result": {
      "sex": "1",
      "customerName": "张三",
      "customerId": "001",
      "age": 30
    }
  }
}

4.2 查询账户列表

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/sink/account" -X POST --data '{"methodName":"getAccountListByCustomerId","params":["001"]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "appServiceId": "/sink/account",
  "traceId": "54d52aac25ef4f86b693cedf592ad56c",
  "parentTraceId": null,
  "timestamp": 1640692642485,
  "data": {
    "result": [
      {
        "accountId": "0001",
        "balance": -34,
        "customerId": "001"
      },
      {
        "accountId": "0002",
        "balance": 200,
        "customerId": "001"
      }
    ]
  }
}

4.3 查询客户信息和账户列表

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/app/personal" -X POST --data '{"methodName":"getCustomerAndAccountList","params":["001"]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "appServiceId": "/app/personal",
  "traceId": "69c3800c50cb41a682d32bd1774328ee",
  "parentTraceId": null,
  "timestamp": 1640692648956,
  "data": {
    "result": {
      "accountList": [
        {
          "accountId": "0001",
          "balance": -34,
          "customerId": "001"
        },
        {
          "accountId": "0002",
          "balance": 200,
          "customerId": "001"
        }
      ],
      "customer": {
        "sex": "1",
        "customerName": "张三",
        "customerId": "001",
        "age": 30
      }
    }
  }
}


三、各层间调用和依赖关系

xBank系统各层间的调用和依赖关系如下图所示:

image.png

可以看到,整个xBank系统从上到下分别是:

  • Source层(对应于DDD架构中的适配接入层):负责聚合服务的接入,包括通讯协议适配和消息格式转换,并统一接入App层的App服务。
  • App层(对应于DDD架构中的应用层):负责服务的整合和编排,对Source层接入的服务,进行服务编排,并通过Sink模块接入要编排的服务。
  • Sink层(对应于DDD架构中的领域层):统一被App服务所调用,实现业务逻辑的处理,或接入外部第三方服务,包括和外部服务对接时的通讯协议适配和消息格式转换。

在下图中标出了在具体开发中的要点,下面会逐一进行讲解:

image.png

主要调用流程如下:

  1. 调用方是通过HTTP POST方式,发送XML格式的报文给Source层的XmlController应用模块❶进行处理;
  2. XmlController服务模块会把报文格式转换成平台内部标准报文,并调用❷ App层的App服务PersonalAppService❹;
  3. PersonalAppService服务会进行服务编排,分别调用❺Sink层的CustomerSinkService服务❼、AccountSinkService服务、Payment1SinkService服务、Payment2SinkService服务。

1、Source层的XmlController

XmlController是一个RestController应用,主要完成以下处理:

  • 接收调用者发来的HTTP POST请求;
  • 通过消息转换器,把XML报文解包成平台标准格式报文;
  • 调用App层封装好的App服务;
  • 把App服务返回的平台标准格式报文,打包成XML报文并返回请求方。

代码如下:

@RestController
@RequestMapping("/personal")
public class XmlController {
    // 定义一个接口为PersonalAppInterface的,app层服务名为app/personal的服务调用句柄
    private PersonalAppInterface personalAppInterface = SourceClientFactory
            .getAppServiceClient(PersonalAppInterface.class,"/app/personal");
    // 定义一个接口为平台标准JSON接口(类型固定为BizMessageInterface),app层服务名为sink/payment1的服务调用句柄
    private BizMessageInterface payment1SinkInterface = SourceClientFactory
            .getAppServiceClient(BizMessageInterface.class,"/sink/payment1");
    // 获取source层消息格式转换器
    private Converter converter = Converter.getSourceConverter("xml-source");

    @PostMapping(value = "/getCustomerAndAccountList", consumes = "application/xml", produces = "application/xml")
    public String getCustomerAndAccountList(@RequestBody String inMessage) throws BizException {
        // 消息解包操作
        JSONObject jsonObject = this.converter.unpack(inMessage.getBytes());
        String customerId = (String)jsonObject.get("customerId");
        // 调用app层服务
        CustomerAndAccountList customerAndAccountList = this.personalAppInterface.getCustomerAndAccountList(customerId);
        jsonObject = JSONUtil.parseObj(customerAndAccountList);
        // 消息打包并返回
        return new String(this.converter.pack(jsonObject));
    }
	......
}

进阶要点

  • Source模块可以采用可复用的代码框架,把通用的Source通讯适配应用和个性化的Source服务分开,通用的Source通讯适配应用可以参见Biz-SIP源码中source模块中的子模块,目前有rest-source和netty-source,前一个是简单的HTTP RESTful POST接入的模块,后一个是用Netty封装的支持TCP同步短连接的模块。个性化的Source服务是继承并实现SourceServiceInterface接口,在其中的doService()方法实现通讯适配以外的工作,包括格式转换、调用App层服务等。

2、XmlController调用App服务

XmlController是通过申请App服务的调用接口,在申请调用接口时,会采用App层封装好的App服务Interface接口类:

public class XmlController {
    // 定义一个接口为PersonalAppInterface的,app层服务名为app/personal的服务调用接口
    private PersonalAppInterface personalAppInterface = SourceClientFactory
            .getAppServiceClient(PersonalAppInterface.class,"/app/personal");
    
    	......    
        // 调用app层服务
        CustomerAndAccountList customerAndAccountList = this.personalAppInterface.getCustomerAndAccountList(customerId);
		......
    }

进阶要点

  • 调用App服务除了使用约定好的自定义接口调用外,还支持非接口约定调用(即采用平台标准JSON消息),Interface接口类统一采用BizMessageInterface接口类,并统一用call()来调用app层服务。

3、App层的PersonalAppInterface接口类

在上面的XmlController代码中,App服务调用接口在申请时,会要求填入App层的服务接口类PersonalAppInterface,这是在App层定义的,约定了能被Source层调用的App服务接口,如下所示:

public interface PersonalAppInterface {
    public CustomerAndAccountList getCustomerAndAccountList(String customerId);
    public List<Account> getAccountListByCustomerId(String customerId);
    public Customer getCustomer(String customerId);
    public BizMessage send2Payment1(Object message) throws BizException;
    public BizMessage send2Payment2(String tranMode, String tranCode, Object message) throws BizException;
    public Customer getCustomerAndSaf2Payment2(String tranMode, String customerId) throws BizException;
    public Account payout(String accountId,long amount);
    public void payoutForward(String tranMode,String accountId,long amount) throws BizException;
    public void payoutForwardCompensate(JSONObject jsonObject) throws BizException;
    public void payoutBackward(String tranMode,String accountId,long amount) throws BizException;
    public void payoutBackwardCompensate(JSONObject jsonObject) throws BizException;
}

进阶要点

  • App层可以设置Source层调用服务的消息校验,支持域级和服务级的消息校验定义。

4、App层的PersonalAppService类

PersonalAppService服务类是上面PersonalAppInterface的具体实现类,服务接口是在类中具体实现的,主要是对Sink层的服务进行服务编排,如下所示:

@Service
public class PersonalAppService implements PersonalAppInterface {
    private AccountSinkInterface accountSinkInterface = AppClientFactory
            .getSinkClient(AccountSinkInterface.class,"account-sink");
    private CustomerSinkInterface customerSinkInterface = AppClientFactory
            .getSinkClient(CustomerSinkInterface.class,"customer-sink");
    private BizMessageInterface payment1SinkInterface = AppClientFactory
            .getSinkClient(BizMessageInterface.class,"payment1-sink");
    private BizMessageInterface payment2SinkInterface = AppClientFactory
            .getSinkClient(BizMessageInterface.class,"payment2-sink");
    private PersonalAppInterface personalAppDelayInterface = AppClientFactory
            .getDelayAppServiceClient(PersonalAppInterface.class,"/app/personal",
                    0,1000,2000,4000,8000,16000,32000);

    @Override
    public CustomerAndAccountList getCustomerAndAccountList(String customerId) {
        Customer customer = this.customerSinkInterface.getCustomer(customerId);
        List<Account> accountList = this.accountSinkInterface.getAccountListByCustomerId(customerId);
        CustomerAndAccountList customerAndAccountList = new CustomerAndAccountList();
        customerAndAccountList.setCustomer(customer);
        customerAndAccountList.setAccountList(accountList);
        return customerAndAccountList;
    }
    ......
}

PersonalAppService服务类是在app.yml文件中进行设置的:

app.yml

- app-service-id: /app/personal
  type: bean-service
  class-name: com.xbank.app.service.PersonalAppService

进阶要点

  • App服务的接口,除了本节介绍的采用由开发者自己定义的Java接口类以外,还可以统一采用AppBeanInterface接口类,AppBeanInterface接口类是采用平台标准JSON消息作为调用和返回参数,开发者继承AppBeanInterface接口后实现process()接口即可。
  • 在App服务中可以调用延迟服务,延迟服务能实现服务调用的自定义调用次数和重试间隔时间。
  • App层可以打开交易日志服务,根据设置的日志级别,会将交易成功、交易延迟调用、交易失败的状态日志,通过RabbitMQ队列发送给开发者应用,进行各种事后处理。

5、PersonalAppService调用Sink层服务

PersonalAppService类在代码中会申请Sink层服务的调用接口,在申请调用接口时,会采用Sink层封装好的Sink服务Interface接口类:

@Service
public class PersonalAppService implements PersonalAppInterface {
    private CustomerSinkInterface customerSinkInterface = AppClientFactory
            .getSinkClient(CustomerSinkInterface.class,"customer-sink");
    	......
        Customer customer = this.customerSinkInterface.getCustomer(customerId);
		......
}

进阶要点

  • 调用Sink层服务,除了支持普通的rest同步调用外,还支持rabbitmq异步调用(App服务无需等待返回结果)。
  • Sink层服务除了支持基于自定义接口类的调用外,还支持基于平台标准报文格式的调用接口,。
  • 可以在App层定义sink-service类型的App服务,直接绑定并调用Sink层的Sink服务。

6、Sink层的CustomerSinkInterface接口类

在上面的PersonalAppService类代码中,Sink服务调用接口在申请时,会要求填入Sink层的服务接口类CustomerSinkInterface,这是在Sink层定义的,约定了能被App层调用的Sink服务接口,如下所示:

public interface CustomerSinkInterface {
    public Customer getCustomer(String customerId);
}

进阶要点

  • Sink层的内置标准服务接口,除了本节介绍的采用由开发者定义Java接口类以外,还可以统一采用SinkBeanInterface接口,开发者继承SinkBeanInterface接口后实现process()接口即可。SinkBeanInterface接口是采用平台标准JSON对象作为调用和返回参数。

7、Sink层的CustomerSinkService类

CustomerSinkService服务类是上面的CustomerSinkInterface接口的实现类,具体的Sink服务业务逻辑是在这个类中实现的:

@Service
public class CustomerSinkService implements CustomerSinkInterface {
    @Autowired
    private CustomerService custmerService;

    @Override
    public Customer getCustomer(String customerId) {
        Customer customer = this.custmerService.getById(customerId);
        return customer;
    }
}

CustomerSinkService服务类是在sink.yml文件中进行设置的:

sink.yml

- id: customer-sink
  type: rest
  processor: bean
  url: http://customer-sink/sink
  class-name: com.xbank.sink.customer.service.CustomerSinkService

并在application.yml(application-local.yml)文件中,在bizsip.sink-id属性中添加customer-sink,以打开注册的customer-sink接口:

bizsip:
  config-path: /var/bizsip/config
  sink-id: customer-sink

进阶要点:

  • 除了本节介绍的采用基于开发者自定义Java接口类开发的bean类型sink服务以外,还可以统一采用SinkBeanInterface接口类,SinkBeanInterface接口类是采用平台标准JSON消息作为调用和返回参数,开发者实现SinkBeanInterface的process()接口即可。
  • bean服务类和sink-bean服务类如果继承AbstractSinkService类,当前sink的connector和converter是会自动注入的。


四、项目实践:客户域服务的开发

本章节以客户域服务为例,展现开发者从数据库建库到Sink服务、App服务、Source服务开发的大致步骤。

1. 创建数据库

执行项目中xbank-infrastructure/xbank-sql/xbank.sql脚本,以建立xbank演示库。

2. 自动生成数据访问层代码

数据库创建后,用MybatisX插件自动生成数据访问层代码:

image.png

3. Customer领域服务接口和实现类的开发

先在xbank-customer-domain-client子模块中约定customer领域服务接口,这个接口是共享给领域层和应用层的:

image.png


然后在xbank-customer-sink子模块中完成以下工作:

  1. 基于此接口完成CustomerSinkService类的编写;
  2. 完成CustomerSinkApplication启动类的编写;
  3. 配置application.yml相关文件。

image.png

以上就完成了客户领域服务微服务的开发,接下来需要把这个客户领域服务微服务,通过sink接入Biz-SIP平台,配置xbank-app/config/sink.yml,就把CustomerSinkService类挂接到了sink(customer-sink)上:

sink.yml:
- id: customer-sink
  type: rest
  processor: bean
  url: http://customer-sink/sink
  class-name: com.xbank.sink.customer.service.CustomerSinkService

4. Customer领域服务的快速发布

Customer领域服务接入Biz-SIP平台后,能实现在Biz-SIP开放平台接口的快速发布,这需要在xbank-app/config/app.yml中配置一个类型为“sink-service”的App服务:

app.yml:
- app-service-id: /sink/customer
  type: sink-service
  sink-id: customer-sink

注:这类“sink-service”的App服务,命名规范为“/sink/xxx”,xxx为sink名称。
启动Biz-SIP平台和Customer领域服务,就可以直接在Biz-SIP开放平台接口进行访问:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/sink/customer" -X POST --data '{"methodName":"getCustomer","params":["001"]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "appServiceId": "/sink/customer",
  "traceId": "e130a4ffc2894d3a98393971a256246e",
  "parentTraceId": null,
  "timestamp": 1640692649234,
  "data": {
    "result": {
      "sex": "1",
      "customerName": "张三",
      "customerId": "001",
      "age": 30
    }
  }
}

在上传的数据体中,“methodName”为方法名,“params”为输入参数。

5. Customer领域服务在应用层和适配层的定制

通过在app.yml中配置sink-service类型的App服务,能实现已经挂接到Sink的领域服务的快速发布;但是,客户针对应用层和适配层,还是有个性化定制要求,这就涉及到应用层和适配层的定制。
同样以已经挂接到customer-sink上的Customer领域服务为例,先在xbank-app-client子模块中约定应用层接口,这个接口是共享给应用层和适配层的:

image.png

然后在xbank-app模块中基于此接口完成PersonalAppService类的编写:
image.png

在xbank-app/config/app.yml中配置一个类型为“bean-service”的App服务,并类名指定为上面实现的PersonalAppService应用层服务类:

app.yml:
- app-service-id: /app/personal
  type: bean-service
  class-name: com.xbank.app.service.PersonalAppService

最后在适配层中,定制一个controller来对外发布服务:

image.png

开发者可以在适配层的controller类中通过personalAppInterface,直接调用应用层App服务”/app/personal“:

$ curl http://localhost:9001/personal/getCustomer\?customerId=001|jq

{
  "customerId": "001",
  "customerName": "张三",
  "age": 30,
  "sex": "1"
}

五、项目实践:账户域服务的开发

1. Account领域服务的封装

Account领域服务是和Customer领域服务并列的,Account领域服务的封装,依次有以下步骤:

  • 第1步:领域服务接口的约定:在xbank-account-sink-client中编写AccountSinkInterface接口;
  • 第2步:领域服务的实现:创建xbank-account-sink子模块,基于第1步约定的接口,实现AccountSinkService服务类;
  • 第3步:配置sink.yml文件,把AccountSinkService服务类作为Sink连接到Biz-SIP平台;

另外,Account领域Sink服务在封装时,涉及到多个接口调用,对每个接口采用命令执行器(Command Executor),所有的接口实现继承AbstractBeanCmdExe类来实现,这样可以避免把所有的业务逻辑代码写在一个Account服务类中:

image.png

2. Account服务在适配层和应用层的开发和对外暴露

把sink服务,暴露给适配层接口调用,有2种方案:

2.1 方案1:Sink服务透传

通过sink-service类型,打通适配层和应用层:直接把account领域服务所对应的sink,直接暴露给Biz-SIP开放平台接口。

  • 第4步:配置app.yml文件,把account-sink直接作为sink-service暴露给Biz-SIP开放平台接口。

接口测试如下:

2.2 方案2:开发Bean服务类来聚合转发

通过bean-service,打通适配层和应用层:

  • 第4步:应用服务接口的约定:在xbank-personal-app-client中编写PersonalAppInterface接口和CustomerAndAccountList DTO类;
  • 第5步:应用服务的实现:在xbank-app模块中,基于第4步约定的接口实现PersonalAppService服务;
  • 第6步:配置app.yml文件,把第5步开发PersonalAppService服务,通过bean-service的account-sink暴露给适配层;
  • 第7步:在适配层创建xbank-openapi-source子模块,开发PersonalController类,暴露个性化的RESTful接口,并通过SourceClientFactory.getAppServiceClient(PersonalAppInterface.class,"/app/personal")获取应用层服务的调用接口,在适配层中调用应用层约定接口。

接口测试如下:

$ curl http://localhost:9001/personal/getAccountListByCustomerId\?customerId=001|jq

[
  {
    "accountId": "0001",
    "balance": 100,
    "customerId": "001"
  },
  {
    "accountId": "0002",
    "balance": 200,
    "customerId": "001"
  }
]

六、项目实践:客户域和账户域服务在应用层的编排

上面已经分别实现了客户域和账户域服务的开发和部署,在应用层中,能方便地对域服务进行服务编排。

1. App服务类的开发和部署

在xbank-app模块中,我们可以在PersonalAppService类中,方便地进行服务编排,代码如下:

@Service
public class PersonalAppService implements PersonalAppInterface {
    private AccountSinkInterface accountSinkInterface = AppClientFactory
            .getSinkClient(AccountSinkInterface.class,"account-sink");
    private CustomerSinkInterface customerSinkInterface = AppClientFactory
            .getSinkClient(CustomerSinkInterface.class,"customer-sink");
    private BizMessageInterface payment1SinkInterface = AppClientFactory
            .getSinkClient(BizMessageInterface.class,"payment1-sink");
    private BizMessageInterface payment2SinkInterface = AppClientFactory
            .getSinkClient(BizMessageInterface.class,"payment2-sink");
    private PersonalAppInterface personalAppDelayInterface = AppClientFactory
            .getDelayAppServiceClient(PersonalAppInterface.class,"/app/personal",
                    0,1000,2000,4000,8000,16000,32000);

    @Override
    public CustomerAndAccountList getCustomerAndAccountList(String customerId) {
        Customer customer = this.customerSinkInterface.getCustomer(customerId);
        List<Account> accountList = this.accountSinkInterface.getAccountListByCustomerId(customerId);
        CustomerAndAccountList customerAndAccountList = new CustomerAndAccountList();
        customerAndAccountList.setCustomer(customer);
        customerAndAccountList.setAccountList(accountList);
        return customerAndAccountList;
    }
...
}

在getCustomerAndAccountList()方法中,我们可以同时调用客户域的接口customerSinkInterface,也可以调用账户域的接口accountSinkInterface,从而实现多个域服务的混合编排。
PersonalAppService服务类通过bean-service进行挂接到App服务,并进行部署:

app.yml:
- app-service-id: /app/personal
  type: bean-service
  class-name: com.xbank.app.service.PersonalAppService

2. App服务被调用的方式

分别支持Biz-SIP开放平台接口访问和个性化的PersonController实现的REST接口:

2.1 通过Biz-SIP开放平台接口

通过开放平台接口进行调用的测试:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/app/personal" -X POST --data '{"methodName":"getCustomerAndAccountList","params":["001"]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "appServiceId": "/app/personal",
  "traceId": "45c12fecc1b141f68266ffd61fe4d4f3",
  "parentTraceId": null,
  "timestamp": 1640692649888,
  "data": {
    "result": {
      "accountList": [
        {
          "accountId": "0001",
          "balance": -34,
          "customerId": "001"
        },
        {
          "accountId": "0002",
          "balance": 200,
          "customerId": "001"
        }
      ],
      "customer": {
        "sex": "1",
        "customerName": "张三",
        "customerId": "001",
        "age": 30
      }
    }
  }
}

2.2 通过个性化开发定制的Controller

通过个性化PersonController实现的RESTful接口,接口测试如下:

$ curl http://localhost:9001/personal/getCustomerAndAccountList\?customerId=001|jq

{
  "customer": {
    "customerId": "001",
    "customerName": "张三",
    "age": 30,
    "sex": "1"
  },
  "accountList": [
    {
      "accountId": "0001",
      "customerId": "001",
      "balance": 100
    },
    {
      "accountId": "0002",
      "customerId": "001",
      "balance": 200
    }
  ]
}

七、项目实践:支付域服务的开发

1. Payment领域服务的封装

payment领域服务是对接第三方缴费平台的,第三方缴费平台的接口是XML报文格式。
payment领域服务是属于对接第三方的领域服务,前面提到的customer领域服务和account领域服务,主要是内部交易处理的领域服务。
这二类领域服务在开发时有比较大的不同:

  • 对接第三方的领域服务,一般涉及到复杂的通讯接口对接和报文格式转换;
  • 内部交易处理领域服务,应用层和领域层之间,建议采用约定的Interface接口类的调用约定(底层Biz-SIP平台自动转换成RESTful协议和BizMessage标准消息进行交互)。

在开发对接第三方的领域服务时,建议采用基于SinkBeanInterface接口的Sink服务:

public interface SinkBeanInterface {
    /**
     * SinkBean服务调用接口
     * @param jsonObject 传入的消息
     * @return 返回值
     * @throws BizException
     */
    public JSONObject process(JSONObject jsonObject) throws BizException;
}

Payment领域服务的封装,依次有以下步骤:

  • 第1步

领域服务的实现:创建xbank-payment1-sink子模块,Payment1SinkService类由于是继承AbstractSinkService类的,使得当前sink的connector和converter是会自动注入的,在process()中就可以直接调用Converter和Connector,实现代码中对报文的解包和打包::

@Service
public class Payment1SinkService extends AbstractSinkService implements SinkBeanInterface {
    @Override
    public JSONObject process(JSONObject jsonObject) throws BizException {
        log.info("传入消息:\n{}", BizUtils.buildJsonLog(jsonObject));
        byte[] inBytes = this.converter.pack(jsonObject);
        log.info("打包后消息:\n{}", BizUtils.buildHexLog(inBytes));
        JSONObject outJsonObject = this.converter.unpack(inBytes);
        log.info("解包后消息:\n{}", BizUtils.buildJsonLog(outJsonObject));
        return outJsonObject;
    }
}
  • 第2步

在sink.yml文件中,把Payment1SinkService微服务作为Sink连接到Biz-SIP平台:

sink.yml

- id: payment1-sink
  type: rest
  processor: sink-bean
  url: http://payment1-sink/sink
  class-name: com.xbank.sink.payment.service.Payment1SinkService
  converter:
    type: simple-xml

2. Payment服务的快速发布

配置app.yml文件,把payment-sink直接作为sink-service暴露给Biz-SIP开放平台接口:

app.yml

- app-service-id: /sink/payment1
  type: sink-service
  sink-id: payment1-sink

接口测试如下:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/sink/payment1" -X POST --data '{"message":"Hello world!"}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "appServiceId": "/sink/payment1",
  "traceId": "93cde69b1b764b3096d6b9bd64119107",
  "parentTraceId": null,
  "timestamp": 1640692650220,
  "data": {
    "message": "Hello world!"
  }
}

Payment1SinkApplication会打印出日志,收到了完成消息转换后的XML报文:

2021-09-09 10:05:13.265  INFO 10756 --- [nio-8003-exec-1] c.x.d.p.service.Payment1DomainService    : 传入消息:
====+ 01-02-03-04-05-06-07-08-09-10-11-12-13-14-15-16-17-18-19-20 + ====== ASCII  ====== +
0000: 3C 3F 78 6D 6C 20 76 65 72 73 69 6F 6E 3D 22 31 2E 30 22 20 | <?xml version="1.0"  |
0020: 65 6E 63 6F 64 69 6E 67 3D 22 55 54 46 2D 38 22 20 73 74 61 | encoding="UTF-8" sta |
0040: 6E 64 61 6C 6F 6E 65 3D 22 6E 6F 22 3F 3E 3C 72 6F 6F 74 3E | ndalone="no"?><root> |
0060: 3C 6D 65 73 73 61 67 65 3E 48 65 6C 6C 6F 20 77 6F 72 6C 64 | <message>Hello world |
0080: 21 3C 2F 6D 65 73 73 61 67 65 3E 3C 2F 72 6F 6F 74 3E       | !</message></root>.. |

3. payment领域服务在应用层和适配层的定制

通过在service.yml中配置sink-service聚合服务,能实现已经挂接到Sink的领域服务的快速发布。
但是,客户针对应用层和适配层,还是有个性化定制要求,这就涉及到应用层和适配层的定制。
例如我们要实现一个把消息发送给缴费平台的业务,输入的消息包括:交易码、传递的消息。
首先,需要在xbank-personal-app-client子模块中的PersonalAppInterface接口,增加一个消息发送接口send2payment1(),这个接口是共享给应用层和适配层的:

public interface PersonalAppInterface {
    ...
    public BizMessage send2Payment1(Object message) throws BizException;
    ...
}

然后在xbank-app模块中基于此接口方法。在PersonalAppService类实现接口:

public class PersonalAppService implements PersonalAppInterface {
	...
    private BizMessageInterface payment1DomainInterface = AppClientFactory
            .getSinkClient(BizMessageInterface.class,"payment1-sink");
	...
    @Override
    public BizMessage<JSONObject> send2Payment1(Object message) throws BizException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.set("message",message);
        return this.payment1DomainInterface.call(jsonObject);
    }
    ...
}

由于payment领域服务没有另外约定接口,是采用BizMessage标准消息接口,所以是采用BizMessageInterface接口来构建sink调用接口的,并统一通过call()方法来调用领域服务。
由于前面已经在service.yml中把PersonalAppService应用层服务类,配置成了“bean-service”的聚合服务,所以可以直接进行接口调用:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/app/personal" -X POST --data '{"methodName":"send2Payment1","params":["Hello world!"]}' 

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "appServiceId": "/app/personal",
  "traceId": "e84221a881b8486f88ac51e56b0dd4b5",
  "parentTraceId": null,
  "timestamp": 1640692651199,
  "data": {
    "result": {
      "traceId": "e84221a881b8486f88ac51e56b0dd4b5",
      "code": 0,
      "data": {
        "message": "Hello world!"
      },
      "message": "success",
      "appServiceId": "/app/personal",
      "timestamp": 1640692651199
    }
  }
}

Payment1SinkApplication应用会收到消息:

2021-09-09 10:13:44.911  INFO 10756 --- [nio-8003-exec-2] c.x.d.p.service.Payment1DomainService    : 传入消息:
====+ 01-02-03-04-05-06-07-08-09-10-11-12-13-14-15-16-17-18-19-20 + ====== ASCII  ====== +
0000: 3C 3F 78 6D 6C 20 76 65 72 73 69 6F 6E 3D 22 31 2E 30 22 20 | <?xml version="1.0"  |
0020: 65 6E 63 6F 64 69 6E 67 3D 22 55 54 46 2D 38 22 20 73 74 61 | encoding="UTF-8" sta |
0040: 6E 64 61 6C 6F 6E 65 3D 22 6E 6F 22 3F 3E 3C 72 6F 6F 74 3E | ndalone="no"?><root> |
0060: 3C 6D 65 73 73 61 67 65 3E 48 65 6C 6C 6F 20 77 6F 72 6C 64 | <message>Hello world |
0080: 21 3C 2F 6D 65 73 73 61 67 65 3E 3C 2F 72 6F 6F 74 3E       | !</message></root>.. |

在适配层中,在原有的PersonalController类中,增加对“/send2Payment1"请求的实现:

    @GetMapping(value ="/send2Payment1")
    public BizMessage<JSONObject> send2Payment1(String message) throws BizException {
        return this.personalAppInterface.send2Payment1(message);
    }

开发者可以在适配层的PersonalController类中,通过personalAppInterface,把消息发送给缴费平台:

$ curl http://localhost:9001/personal/send2Payment1\?message=hello|jq 

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "appServiceId": "/app/personal",
  "traceId": "96138053c11a41faa86acb286ee6d976",
  "parentTraceId": null,
  "timestamp": 1640700400374,
  "data": {
    "message": "hello"
  }
}

在适配层中,还可以通过配置sink-service类型的聚合服务,直接调用领域层的Sink服务。例如可以在原有的PersonalController类中,直接向应用层发起JSONObject类型的平台报文:

@RestController
@RequestMapping("/personal")
public class OpenapiController {
	...
    private BizMessageInterface payment1SinkInterface = SourceClientFactory
            .getAppServiceClient(BizMessageInterface.class,"/sink/payment1");
	...
    @GetMapping(value ="/send2Payment")
    public BizMessage<JSONObject> send2Payment(String message) throws BizException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.set("message",message);
        return this.payment1SinkInterface.call(jsonObject);
    }
}

开发者可以在适配层的OpenapiController类中,通过payment1SinkInterface,把消息发送给缴费平台:

$ curl http://localhost:9001/personal/send2Payment\?message=hello|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "appServiceId": "/sink/payment1",
  "traceId": "13503e8193604d8b88eb48615964a148",
  "parentTraceId": null,
  "timestamp": 1640700717374,
  "data": {
    "message": "hello"
  }
}

八、项目实践:应用服务对延迟服务的组装

1. SAF存储转发的实现

在领域层中,开发Payment2SinkService,根据tranMode交易模式,实现处理超时、处理失败和处理成功的各种异常情况:

@Service
public class Payment2SinkService implements JSONObjectSinkBeanInterface {
    @Autowired
    private TimeoutCmdExe timeoutCmdExe;
    @Autowired
    private TimeoutAndFailCmdExe timeoutAndFailCmdExe;
    @Autowired
    private TimeoutAndSuccessCmdExe timeoutAndSuccessCmdExe;
    @Autowired
    private SuccessCmdExe successCmdExe;

    @Override
    public JSONObject process(JSONObject jsonObject) throws BizException {
        log.info("传入消息:\n{}", jsonObject.toString());
        AbstractSinkBeanCmdExe sinkBeanCmdExe;
        String tranMode = (String)jsonObject.get("tranMode");
        switch (tranMode) {
            case "timeout":
                // 收到交易后,永远返回超时
                return timeoutCmdExe.execute(jsonObject);
            case "3timeout-fail":
                // 收到交易后,前3次返回超时,第4次返回失败码
                return timeoutAndFailCmdExe.execute(jsonObject);
            case "3timeout-success":
                // 收到交易后,前3次返回超时,第4次成功返回原报文
                return timeoutAndSuccessCmdExe.execute(jsonObject);
            default:
                //其它情况,成功返回原报文
                return successCmdExe.execute(jsonObject);
        }
    }
}

注意,Payment2领域服务在封装时,涉及到多个接口调用,对每个接口采用命令执行器(Command Executor),这里是传入标准的JSONObject数据,所以所有的接口实现继承AbstractSinkBeanCmdExe类来实现:

image.png

在应用层中,在原有的PersonalAppInterface应用层接口、PersonalAppService服务类中,增加send2Payment2()、getCustomerAndSaf2Payment2()这2个方法接口和实现,通过personalAppDelayInterface延迟调用接口,实现对原有send2Payment2()方法的多次SAF调用:

@Service
public class PersonalAppService implements PersonalAppInterface {
    ...
        
    private BizMessageInterface payment2SinkInterface = AppClientFactory
            .getSinkClient(BizMessageInterface.class,"payment2-sink");
    private PersonalAppInterface personalAppDelayInterface = AppClientFactory
            .getDelayAppServiceClient(PersonalAppInterface.class,"/app/personal",
                    0,1000,2000,4000,8000,16000,32000);
	...
        
    @Override
    public BizMessage send2Payment2(String tranMode, String tranCode, Object message) throws BizException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.set("tranCode",tranCode);
        jsonObject.set("tranMode",tranMode);
        jsonObject.set("message",message);
        return this.payment2SinkInterface.call(jsonObject);
    }

    @Override
    public Customer getCustomerAndSaf2Payment2(String tranMode,String customerId) throws BizException {
        Customer customer = this.customerSinkInterface.getCustomer(customerId);
        this.personalAppDelayInterface.send2Payment2(tranMode,"send-customer", customer);
        return customer;
    }
    ...
}

传入交易模式为”timeout“(收到交易后,永远返回超时),这将导致发送7次,最后交易状态为失败:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/app/personal" -X POST --data '{"methodName":"getCustomerAndSaf2Payment2","params":["timeout","001"]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "traceId": "c1d2ffb7f0b44bf4b0cb7e13e5c0c0fa",
  "parentTraceId": null,
  "timestamp": 1631274896174,
  "data": {
    "result": {
      "sex": "1",
      "customerName": "张三",
      "customerId": "001",
      "age": 30
    }
  }
}

Payment2SinkApplication日志:

2021-09-10 19:54:56.650  INFO 14993 --- [nio-8004-exec-3] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"timeout","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:54:56.651  INFO 14993 --- [nio-8004-exec-3] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:54:57.753  INFO 14993 --- [nio-8004-exec-4] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"timeout","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:54:57.754  INFO 14993 --- [nio-8004-exec-4] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:54:59.920  INFO 14993 --- [nio-8004-exec-5] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"timeout","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:54:59.920  INFO 14993 --- [nio-8004-exec-5] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:55:04.015  INFO 14993 --- [nio-8004-exec-6] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"timeout","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:55:04.015  INFO 14993 --- [nio-8004-exec-6] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:55:12.041  INFO 14993 --- [nio-8004-exec-7] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"timeout","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:55:12.042  INFO 14993 --- [nio-8004-exec-7] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:55:28.080  INFO 14993 --- [nio-8004-exec-8] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"timeout","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:55:28.080  INFO 14993 --- [nio-8004-exec-8] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:56:00.228  INFO 14993 --- [nio-8004-exec-9] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"timeout","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:56:00.228  INFO 14993 --- [nio-8004-exec-9] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!


传入交易码为”3timeout-fail“(收到交易后,前3次返回超时,第4次返回失败),这将导致发送4次,最后交易状态为失败:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/app/personal" -X POST --data '{"methodName":"getCustomerAndSaf2Payment2","params":["3timeout-fail","001"]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "traceId": "d3589b169d654a409c2f7d15f83f5113",
  "parentTraceId": null,
  "timestamp": 1631275051123,
  "data": {
    "result": {
      "sex": "1",
      "customerName": "张三",
      "customerId": "001",
      "age": 30
    }
  }
}

Payment2SinkApplication日志:

2021-09-10 19:57:31.379  INFO 14993 --- [nio-8004-exec-1] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"3timeout-fail","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:57:31.379  INFO 14993 --- [nio-8004-exec-1] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:57:32.647  INFO 14993 --- [nio-8004-exec-2] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"3timeout-fail","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:57:32.647  INFO 14993 --- [nio-8004-exec-2] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:57:34.672  INFO 14993 --- [nio-8004-exec-3] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"3timeout-fail","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:57:34.673  INFO 14993 --- [nio-8004-exec-3] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:57:38.733  INFO 14993 --- [nio-8004-exec-4] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"3timeout-fail","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:57:38.733  INFO 14993 --- [nio-8004-exec-4] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回交易处理失败!

传入交易码为”3timeout-success“(收到交易后,前3次返回超时,第4次成功返回原报文),这将导致发送4次,最后交易状态为成功:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/app/personal" -X POST --data '{"methodName":"getCustomerAndSaf2Payment2","params":["3timeout-success","001"]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "traceId": "75b76cf518164964bedf477ea488698c",
  "parentTraceId": null,
  "timestamp": 1631275117934,
  "data": {
    "result": {
      "sex": "1",
      "customerName": "张三",
      "customerId": "001",
      "age": 30
    }
  }
}

Payment2SinkApplication日志:

2021-09-10 19:58:38.146  INFO 14993 --- [nio-8004-exec-5] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"3timeout-success","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:58:38.146  INFO 14993 --- [nio-8004-exec-5] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:58:39.200  INFO 14993 --- [nio-8004-exec-6] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"3timeout-success","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:58:39.200  INFO 14993 --- [nio-8004-exec-6] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:58:41.409  INFO 14993 --- [nio-8004-exec-7] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"3timeout-success","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:58:41.409  INFO 14993 --- [nio-8004-exec-7] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回超时异常!
2021-09-10 19:58:45.539  INFO 14993 --- [nio-8004-exec-8] c.x.d.p.service.Payment2DomainService    : 传入消息:
{"tranMode":"3timeout-success","message":{"sex":"1","customerName":"张三","customerId":"001","age":30},"tranCode":"send-customer"}
2021-09-10 19:58:45.539  INFO 14993 --- [nio-8004-exec-8] c.x.d.p.service.Payment2DomainService    : 交易:send-customer,返回交易成功!

2. 向前补偿的实现

向前补偿机制是指当交易无应答时,系统会对原交易进行查询,如查询超时会重试多次。如原交易成功则继续后续交易,如原交易失败则对前续交易进行交易补偿,向前补偿一般是针对第三方接入方无法提供补偿交易时的解决方案。
在金融领域,这类交易补偿动作常被称为“冲正”。
在应用层中,在原有的PersonalAppInterface应用层接口、PersonalAppService服务类中,增加payoutForward()、payoutForwardCompensate()这2个方法接口和实现,通过personalAppDelayInterface延迟调用接口,实现对原有payoutForwardCompensate()方法的多次重复延迟调用:

@Service
public class PersonalAppService implements PersonalAppInterface {
    private AccountSinkInterface accountSinkInterface = AppClientFactory
            .getSinkClient(AccountSinkInterface.class,"account-sink");
    private CustomerSinkInterface customerSinkInterface = AppClientFactory
            .getSinkClient(CustomerSinkInterface.class,"customer-sink");
    private BizMessageInterface payment1SinkInterface = AppClientFactory
            .getSinkClient(BizMessageInterface.class,"payment1-sink");
    private BizMessageInterface payment2SinkInterface = AppClientFactory
            .getSinkClient(BizMessageInterface.class,"payment2-sink");
    private PersonalAppInterface personalAppDelayInterface = AppClientFactory
            .getDelayAppServiceClient(PersonalAppInterface.class,"/app/personal",
                    0,1000,2000,4000,8000,16000,32000);
    
    ......
        
	@Override
    public void payoutForward(String tranMode,String accountId, long amount) throws BizException {
        log.info("account出金:{},{}",accountId,amount);
        this.accountDomainInterface.payout(accountId,amount);
        JSONObject jsonObject = new JSONObject();
        jsonObject.set("tranCode","pay");
        jsonObject.set("tranMode",tranMode);
        jsonObject.set("accountId",accountId);
        jsonObject.set("tranAmount",amount);
        BizMessage<JSONObject> bizMessage = null;
        try {
            log.info("payment缴费...");
            bizMessage = this.payment2SinkInterface.call(jsonObject);
        } catch (BizException e) {
            if (e.isTimeOutException()) {
                log.info("payment交易超时,开始payout补偿...");
                this.personalAppDelayInterface.payoutForwardCompensate(jsonObject);
                return;
            }
            else {
                throw e;
            }
        }
        log.info("payment缴费成功!");
        log.info("payout成功!");
    }

    @Override
    public void payoutForwardCompensate(JSONObject jsonObject)  throws BizException{
        jsonObject.set("tranCode","pay-query");
        BizMessage<JSONObject> bizMessage = null;
        try {
            log.info("payment查询缴费订单...");
            bizMessage = this.payment2SinkInterface.call(jsonObject);
        } catch (BizException e) {
            if (e.isTimeOutException()) {
                log.info("payment交易超时...");
                throw e;
            }
            else {
                log.info("payment查询缴费订单返回错误(表示对方订单没执行)...");
                String accountId = (String)jsonObject.get("accountId");
                long amount = (Integer) jsonObject.get("tranAmount");
                log.info("account出金补偿:{},{}",accountId,amount);
                this.accountDomainInterface.payoutCompensation(accountId,amount);
                return;
            }
        }
        log.info("payment查询缴费订单成功(表示对方订单已执行)");
        log.info("payout成功!");
    }

传入交易模式为”3timeout-success“(收到订单交易后,前3次返回超时,第4次成功返回模拟订单已执行),这将导致发送4次,最后交易状态为成功:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/app/personal" -X POST --data '{"methodName":"payoutForward","params":["3timeout-success","0001",1]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "traceId": "e0127a25e2514da682ad77042917087a",
  "parentTraceId": null,
  "timestamp": 1631275840559,
  "data": {}
}

XbankAppApplication日志:

[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:10:40.560 INFO 16503 [11034350FE38437BB459F1E8DC6CCE17] [http-nio-8888-exec-5] c.xbank.app.service.PersonalAppService   account出金:0001,1
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:10:40.822 INFO 16503 [11034350FE38437BB459F1E8DC6CCE17] [http-nio-8888-exec-5] c.xbank.app.service.PersonalAppService   payment缴费...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:10:40.831 INFO 16503 [11034350FE38437BB459F1E8DC6CCE17] [http-nio-8888-exec-5] c.xbank.app.service.PersonalAppService   payment交易超时,开始payout补偿...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:10:42.086 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-4] c.xbank.app.service.PersonalAppService   payment查询缴费订单...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:10:42.095 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-4] c.xbank.app.service.PersonalAppService   payment交易超时...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:10:43.268 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-3] c.xbank.app.service.PersonalAppService   payment查询缴费订单...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:10:43.277 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-3] c.xbank.app.service.PersonalAppService   payment交易超时...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:10:45.296 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-2] c.xbank.app.service.PersonalAppService   payment查询缴费订单...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:10:45.306 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-2] c.xbank.app.service.PersonalAppService   payment查询缴费订单成功(表示对方订单已执行)
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:10:45.306 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-2] c.xbank.app.service.PersonalAppService   payout成功!

传入交易模式为”3timeout-success“(收到订单交易后,前3次返回超时,第4次成功返回模拟订单已执行),这将导致发送4次,最后交易状态为成功:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/app/personal" -X POST --data '{"methodName":"payoutForward","params":["3timeout-fail","0001",1]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "traceId": "a04163bf05484fa28071650a085d4124",
  "parentTraceId": null,
  "timestamp": 1631275997342,
  "data": {}
}

XbankAppApplication日志:

[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:13:17.342 INFO 16503 [5E5951618A27430684C5552E1F782D8B] [http-nio-8888-exec-7] c.xbank.app.service.PersonalAppService   account出金:0001,1
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:13:17.568 INFO 16503 [5E5951618A27430684C5552E1F782D8B] [http-nio-8888-exec-7] c.xbank.app.service.PersonalAppService   payment缴费...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:13:17.579 INFO 16503 [5E5951618A27430684C5552E1F782D8B] [http-nio-8888-exec-7] c.xbank.app.service.PersonalAppService   payment交易超时,开始payout补偿...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:13:17.775 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-5] c.xbank.app.service.PersonalAppService   payment查询缴费订单...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:13:17.788 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-5] c.xbank.app.service.PersonalAppService   payment交易超时...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:13:18.827 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] c.xbank.app.service.PersonalAppService   payment查询缴费订单...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:13:18.836 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] c.xbank.app.service.PersonalAppService   payment交易超时...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:13:20.890 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-4] c.xbank.app.service.PersonalAppService   payment查询缴费订单...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:13:20.900 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-4] c.xbank.app.service.PersonalAppService   payment查询缴费订单返回错误(表示对方订单没执行)...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:13:20.901 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-4] c.xbank.app.service.PersonalAppService   account出金补偿:0001,1

3. 向后补偿的实现

向后补偿机制是指当交易无应答时,系统会对原交易发起多次交易补偿重试,如补偿交易成功则对前续交易进行补偿。
在应用层中,在原有的PersonalAppInterface应用层接口、PersonalAppService服务类中,增加payoutBackward()、payoutBackwardCompensate()这2个方法接口和实现,通过personalAppDelayInterface延迟调用接口,实现对原有payoutForwardCompensate()方法的多次重复延迟调用:

@Service
public class PersonalAppService implements PersonalAppInterface {
    private AccountSinkInterface accountSinkInterface = AppClientFactory
            .getSinkClient(AccountSinkInterface.class,"account-sink");
    private CustomerSinkInterface customerSinkInterface = AppClientFactory
            .getSinkClient(CustomerSinkInterface.class,"customer-sink");
    private BizMessageInterface payment1SinkInterface = AppClientFactory
            .getSinkClient(BizMessageInterface.class,"payment1-sink");
    private BizMessageInterface payment2SinkInterface = AppClientFactory
            .getSinkClient(BizMessageInterface.class,"payment2-sink");
    private PersonalAppInterface personalAppDelayInterface = AppClientFactory
            .getDelayAppServiceClient(PersonalAppInterface.class,"/app/personal",
                    0,1000,2000,4000,8000,16000,32000);
    
    ......
        
	@Override
    public void payoutBackward(String tranMode, String accountId, long amount) throws BizException {
        log.info("account出金:{},{}",accountId,amount);
        this.accountDomainInterface.payout(accountId,amount);
        JSONObject jsonObject = new JSONObject();
        jsonObject.set("tranCode","pay");
        jsonObject.set("tranMode",tranMode);
        jsonObject.set("accountId",accountId);
        jsonObject.set("tranAmount",amount);
        BizMessage<JSONObject> bizMessage = null;
        try {
            log.info("payment缴费...");
            bizMessage = this.payment2SinkInterface.call(jsonObject);
        } catch (BizException e) {
            if (e.isTimeOutException()) {
                log.info("payment交易超时,开始payout冲正...");
                this.personalAppDelayInterface.payoutBackwardCompensate(jsonObject);
                return;
            }
            else {
                log.info("payment缴费交易返回错误");
                log.info("account出金补偿:{},{}",accountId,amount);
                this.accountDomainInterface.payoutCompensation(accountId,amount);
                log.info("payout交易补偿成功!");
                return;
            }
        }
        log.info("payment缴费成功");
        log.info("payout成功!");
    }

    @Override
    public void payoutBackwardCompensate(JSONObject jsonObject) throws BizException {
        jsonObject.set("tranCode","pay-reversal");
        BizMessage<JSONObject> bizMessage;
        try {
            log.info("payment缴费补偿...");
            bizMessage = this.payment2SinkInterface.call(jsonObject);
            log.info("payment缴费补偿成功");
        } catch (BizException e) {
            if (e.isTimeOutException()) {
                log.info("payment缴费补偿交易超时...");
                throw e;
            }
            log.info("payment缴费补偿交易失败,需要人工干预调整!");
            return;
        }
        String accountId = (String)jsonObject.get("accountId");
        long amount = (Integer)jsonObject.get("tranAmount");
        log.info("account出金补偿:{},{}",accountId,amount);
        this.accountDomainInterface.payoutCompensation(accountId,amount);
        log.info("payout补偿成功!");
    }

传入交易模式为”3timeout-success“(收到订单交易后,前3次返回补偿交易超时,第4次成功返回补偿交易执行成功),这将导致发送4次,最后交易状态为成功:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/app/personal" -X POST --data '{"methodName":"payoutBackward","params":["3timeout-success","0001",1]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "traceId": "26c58fe44b3f4ceba462762494fa5981",
  "parentTraceId": null,
  "timestamp": 1631276405513,
  "data": {}
}

XbankAppApplication日志:

[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:05.513 INFO 16503 [84936F44451449C5A92183EF93C3690C] [http-nio-8888-exec-2] c.xbank.app.service.PersonalAppService   account出金:0001,1
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:05.952 INFO 16503 [84936F44451449C5A92183EF93C3690C] [http-nio-8888-exec-2] c.xbank.app.service.PersonalAppService   payment缴费...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:05.961 INFO 16503 [84936F44451449C5A92183EF93C3690C] [http-nio-8888-exec-2] c.xbank.app.service.PersonalAppService   payment交易超时,开始payout冲正...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:06.396 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] c.xbank.app.service.PersonalAppService   payment缴费补偿...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:06.405 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] c.xbank.app.service.PersonalAppService   payment缴费补偿交易超时...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:07.468 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-4] c.xbank.app.service.PersonalAppService   payment缴费补偿...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:07.475 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-4] c.xbank.app.service.PersonalAppService   payment缴费补偿交易超时...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:09.560 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-3] c.xbank.app.service.PersonalAppService   payment缴费补偿...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:09.570 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-3] c.xbank.app.service.PersonalAppService   payment缴费补偿成功
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:09.571 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-3] c.xbank.app.service.PersonalAppService   account出金补偿:0001,1
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:20:09.726 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-3] c.xbank.app.service.PersonalAppService   payout补偿成功!

传入交易模式为”3timeout-fail“(收到订单交易后,前3次返回超时,第4次成功返回模拟补偿交易失败),这将导致发送4次,最后交易状态为失败:

$ curl -H "Content-Type:application/json" -H "Biz-Service-Id:/app/personal" -X POST --data '{"methodName":"payoutBackward","params":["3timeout-fail","0001",1]}' http://localhost:8888/api|jq

{
  "code": 0,
  "message": "success",
  "extMessage": null,
  "traceId": "40aed8f06bba436ba67ff93e769d5a33",
  "parentTraceId": null,
  "timestamp": 1631276555599,
  "data": {}
}

XbankAppApplication日志:

[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:22:35.599 INFO 16503 [B54A535614A64492A7A00D987F12B529] [http-nio-8888-exec-4] c.xbank.app.service.PersonalAppService   account出金:0001,1
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:22:35.760 INFO 16503 [B54A535614A64492A7A00D987F12B529] [http-nio-8888-exec-4] c.xbank.app.service.PersonalAppService   payment缴费...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:22:35.767 INFO 16503 [B54A535614A64492A7A00D987F12B529] [http-nio-8888-exec-4] c.xbank.app.service.PersonalAppService   payment交易超时,开始payout冲正...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:22:35.979 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-2] c.xbank.app.service.PersonalAppService   payment缴费补偿...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:22:35.993 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-2] c.xbank.app.service.PersonalAppService   payment缴费补偿交易超时...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:22:37.073 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-5] c.xbank.app.service.PersonalAppService   payment缴费补偿...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:22:37.083 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-5] c.xbank.app.service.PersonalAppService   payment缴费补偿交易超时...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:22:39.111 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] c.xbank.app.service.PersonalAppService   payment缴费补偿...
[bizsip-integrator:192.169.1.101:8888] 2021-09-10 20:22:39.121 INFO 16503 [] [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1] c.xbank.app.service.PersonalAppService   payment缴费补偿交易失败,需要人工干预调整!
上一页
下一页