65.9K
CodeProject 正在变化。 阅读更多。
Home

模拟金融中间件系统

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2015年11月5日

CPOL

10分钟阅读

viewsIcon

16426

downloadIcon

309

本文通过提供金融中间件系统的模拟层,有助于减少离岸开发工作。

目录

引言

获得金融应用程序的经验绝非寻常。您必须考虑许多开发人员通常会忽略或不知道的额外事项。例如,满足监管要求,如审计日志、制作者-检查者规则、在执行金融交易及其撤销时采取极端安全措施、安全性、数据归档、反洗钱、KYC 等。

集成主要是任何金融应用程序的核心部分,因为无论如何您都必须与银行主机或中间件集成,这绝非易事。请记住,主机系统通常指核心银行系统,而中间件实际上是与 ATM、SMS、电话银行、IVR、WAP 等主机系统通信的渠道集成器。

在开发环境中,最严峻的挑战是在异地编写集成代码,因为中间件或主机系统在开发中心不可用。这一关键限制迫使公司在现场完成所有集成工作,这显然会增加开发和生产后支持的成本,因为在大多数情况下,需要现场调查修复问题。

本文通过在开发中心创建一个模拟主机系统/中间件系统的虚拟环境,来创建模拟集成层,从而帮助缓解这一问题。

通过创建模拟层,我们可以在开发中心轻松模拟中间件,这极大地有助于开发/支持活动。

模拟集成层

此集成层应遵循以下目标进行设计。

  • 前端应用程序在此模拟层方面不得以任何方式更改,并将其视为与生产环境完全相同的真实集成层。
  • 该层应该是易于更改的,这意味着添加新交易应该非常容易,并且根本不需要任何开发工作。

异地开发中心对前端应用程序进行此模拟集成层的定制将破坏目标,因为由于应用程序中的定制,我们永远无法确定其质量。

如果此模拟层不易于更改,则意味着我们通过维护此层而增加了项目的开发成本,最终将导致开发团队在现场进行活动并放弃此模拟层。

设计

主机系统通常通过 TCP/IP 上的消息驱动通信协议进行通信。这可以是 ISO 8583 消息格式、简单的字符分隔文本格式或其他供应商驱动的格式。每条消息都标记为一个请求/响应格式的事务。每个请求/响应都有标头和数据部分。典型的消息标头可能如下所示:

Content

字节长度

描述

消息大小(字节)

4

0600(如果消息长度为 600)

消息类型

2

01-请求

02-响应

交易代码

4

0001-余额查询

日期时间

14

YYYYMMDDHHmmss [20150901112910]

STAN

6

123456

样本消息标头如下详细说明,以便于理解。

现在我们理解了典型的主机系统是如何工作的,我们需要设计模拟层,使其包含以下模块:

  • 用于前端和模拟层之间通信的 TCP/IP 模块
  • 组装请求/响应消息的机制
  • 包含虚拟消费者信息的数��部分

TCP/IP 模块

TCP/IP 模块是最简单的,可以使用网上随处可见的示例来构建。我们需要了解这是一个正在构建的模拟层,用于异地开发,因此不需要生产级别的性能要求。一个简单的 TCP/IP 模块足以满足此目的。

组装请求/响应消息

这是模拟层的核心部分,必须以一种可以即时进行添加/修改的方式进行设计。XML 在数据驱动的应用程序方面具有巨大的灵活性和强大的功能,非常适合此用途。让我们将这个关键的 XML 文件命名为 TransactonMetaInfo.xml 并开始创建它。

一个基本版本可以像这样:

<Root Client="ABC Bank" Product="ACME Product">
   <Header Type="Request"
      <Field Name="MessageType" EvalFunction="" DefaultValue="" />
      <Field Name="Transactioncode" EvalFunction="" DefaultValue="" />
      <Field Name="TranDateTime" EvalFunction="" DefaultValue="" />
      <Field Name="STAN" EvalFunction="" DefaultValue="" />
   </Header>
   <Header Type="Response">
      <Field Name="MessageType" EvalFunction="" DefaultValue="" />
      <Field Name="Transactioncode" EvalFunction="" DefaultValue="" />
      <Field Name="TranDateTime" EvalFunction="" DefaultValue="" />
      <Field Name="STAN" EvalFunction="" DefaultValue="" />
   </Header>
   <Transaction DispName="Balance Inquiry" Code="9999">
      <Request Code="11" Example="">
         <Header Type="Request"/>
         <Field Name="CustomerPan" EvalFunction="" DefaultValue="" />
      </Request>
      <Response Code="22" Example="">
         <Header Type="Response"/>
         <Field Name="ResponseCode" EvalFunction="" DefaultValue=""/>
         <Field Name="Balance" EvalFunction="" DefaultValue=""/>
      </Response>
   </Transaction>
</Root>

XML 非常直接。根标记定义了客户端和此 XML 所绑定产品的名称。这消除了风险,以防模拟层被用于多个客户,例如美国银行、花旗银行等。

我们稍后将讨论 Header 标签;现在,最重要的设计元素是交易。每笔交易都服务于特定的业务需求。例如,余额查询是一笔服务于特定需求的交易,其交易代码可能是 9999。同样,我们也可以进行另一笔交易,如账单查询或账单支付,以此类推。每笔交易都由请求和响应消息组成。让我们通过创建一个场景来理解这一点。假设互联网银行是源渠道,创建一个余额查询交易的请求消息并发送到中间件,中间件将此请求转发给实际处理请求的银行主机,主机反过来创建一个响应消息发回中间件,中间件重新格式化响应并将其发送回源渠道(互联网银行)。这个完整的循环称为一次交易。

请求包含一个标头和其他参数。类似地,响应也包含一个标头和其他信息。由于特定客户端实现的后续所有交易的标头都是常量,那么自然的做法是在 XML 的开头包含两种类型的标头 {请求/响应}。

XML 提供了极大的灵活性,可以即时添加/更新/删除字段,甚至可以实时创建整个交易集。您所需要做的就是修改字段。

字段可以有一个默认值。如果提供了默认值,则默认值的优先级很高,这意味着模拟层会忽略消息中该字段对应的数据,并强制该字段具有默认值。字段还可以有一个求值函数。Eval 函数是内置的系统函数,在收到请求消息时可以在运行时触发。

引入 Header 标签是为了识别所有交易集共有的字段。由于其字段中的值不同,Headers 还可以进一步分为请求和响应类型。独立的 Headers 也提供了修改单个 Headers 的灵活性,也就是说,如果响应 Headers 包含额外的内容,也可以实现这一点。

此时,尽管我们已经构建了一个基本设计,但它存在局限性。当模拟层收到消息时,每个请求都需要被适当地自动拆分。这需要额外的求值函数,以便模拟引擎能够轻松地完成这项工作。我们提供以下函数:

命令函数

描述

GetMessageFieldByIndex

顾名思义,此 eval 函数按索引在请求中搜索字段并返回位于该索引位置的值。例如:对于请求消息:0064~01~0001~20151009132331~123456~010000001,GetMessageFieldByIndex(2) 将返回 **0001**。

Date.Now

此函数以 yyyymmddhhmmss 格式返回当前日期和时间。

ResolveXPath

此函数对提供的 XPath 进行求值,并必须返回单个值。

让我们根据新获得的知识修改我们的 XML。

<Root Client="ABC Bank" Product="ACME Product">
   <Header Type="Request"
      <Field Name="MessageType" EvalFunction="" DefaultValue="1" />
      <Field Name="Transactioncode" EvalFunction="GetMessageFieldByIndex(1)" DefaultValue="" />
      <Field Name="TranDateTime" EvalFunction="GetMessageFieldByIndex(3)" DefaultValue="" />
      <Field Name="STAN" EvalFunction="GetMessageFieldByIndex(4)" DefaultValue="" />
   </Header>
   <Header Type="Response">
      <Field Name="MessageType" EvalFunction="" DefaultValue="2" />
      <Field Name="Transactioncode" EvalFunction="" DefaultValue="GetMessageFieldByIndex(1)" />
      <Field Name="TranDateTime" EvalFunction="Date.Now" DefaultValue="" />
      <Field Name="STAN" EvalFunction="GetMessageFieldByIndex(4)" DefaultValue="" />
   </Header>
   <Transaction DispName="Balance Inquiry" Code="9999">
      <Request Code="11" Example="">
         <Header Type="Request"/>
            <Field Name="CustomerPan" EvalFunction="GetMessageFieldByIndex(5)" DefaultValue="" />
      </Request>
      <Response Code="22" Example="">
         <Header Type="Response"/>
     <Field Name="ResponseCode" EvalFunction="" DefaultValue="0"/>
     <Field Name="Balance" EvalFunction="" DefaultValue=""/>
      </Response>
   </Transaction>
</Root>

 

上述方法现在带来了一种自动创建请求和响应列表的方法,该方法基于原始请求消息。唯一剩下的就是客户数据,即余额。填补这个空白很容易,我们只需要使用客户 PAN 筛选客户数据并获取可用余额,而实现这一点并使其易于修改的最佳方法是 XPATH。

现在唯一缺失的部分是客户数据。如果返回硬编码的消息,我们的模拟层将非常有限。该层应该了解客户,并对不同客户有不同的行为。这也有助于进行全面的测试,因为不同的客户可以配置不同的值来产生不同的输出。一个明显的测试是用零余额配置一个客户来测试余额不足的情况。

如果我们引入一个客户标签到我们的 XML 中,如下简要概述,就可以轻松实现这一点。

Customer 标签非常灵活,因为它允许添加新客户及其关联信息。它还支持创建新的数据层次结构。最后一个缺失的元素是创建消息字段和客户之间的链接机制。我们也可以这样做,然后完成。

<Root Client="ABC Bank" Product="ACME Product">
   <Header Type="Request"
      <Field Name="MessageType" EvalFunction="" DefaultValue="1" />
      <Field Name="Transactioncode" EvalFunction="GetMessageFieldByIndex(1)" DefaultValue="" />
      <Field Name="TranDateTime" EvalFunction="GetMessageFieldByIndex(3)" DefaultValue="" />
      <Field Name="STAN" EvalFunction="GetMessageFieldByIndex(4)" DefaultValue="" />
   </Header>
   <Header Type="Response">
      <Field Name="MessageType" EvalFunction="" DefaultValue="2" />
      <Field Name="Transactioncode" EvalFunction="" DefaultValue="GetMessageFieldByIndex(1)" />
      <Field Name="TranDateTime" EvalFunction="Date.Now" DefaultValue="" />
      <Field Name="STAN" EvalFunction="GetMessageFieldByIndex(4)" DefaultValue="" />
   </Header>
   <Transaction DispName="Balance Inquiry" Code="9999">
      <Request Code="11" Example="">
         <Header Type="Request"/>
            <Field Name="CustomerPan" EvalFunction="GetMessageFieldByIndex(5)" DefaultValue="" />
      </Request>
      <Response Code="22" Example="">
         <Header Type="Response"/>
     <Field Name="ResponseCode" EvalFunction="" DefaultValue="0"/>
     <Field Name="Balance" 
       EvalFunction="ResolveXPath('//Customers/Customer/Account[@PAN={0}]/@Balance, GetMessageFieldByIndex(5));" DefaultValue=""/>
      </Response>
   </Transaction>
   <Customers>
      <Customer UserID="testUser1" Name="Test User 1" DateOfBirth="01/01/1995" 
                Gender="MA" PhoneMobile="03339999999" AddressOffice="" 
                CNIC="0123456789123" Email="testUser1@test.com" 
                Fax="" TotalAccounts="1">
         <Account BranchCode="0101" Number="010000001" IBAN="" PAN=”010000001”
               Currency="PKR" Balance="100" AccountTitle="Test User 1"              
               AccountType="SAVING" BranchName="Head Office Branch"/>
      </Customer>
    <Customer UserID=" testUser2" Name="Test User 2" DateOfBirth="10/01/1996" 
              Gender="MA" PhoneMobile="03349999999" AddressOffice="" 
              CNIC="0123456789124" Email="testUser2@test.com" 
              Fax="" TotalAccounts="1">
        <Account BranchCode="0101" Number="010000002" IBAN="" PAN=”010000002”
               Currency="PKR" Balance="100" AccountTitle="Test User 1"              
               AccountType="SAVING" BranchName="Head Office Branch"/>
      </Customer>
   <Customers>
</Root>

如果我们仔细查看下面的字段标签,您就可以轻松理解字段和数据之间的链接是如何创建的。

<Field Name="Balance" 
       EvalFunction="ResolveXPath('//Customers/Customer/Account[@PAN={0}]/@Balance, GetMessageFieldByIndex(4));" 
       DefaultValue=""/>

是的,正是 XPath 再次派上了用场,用于获取特定于数据的结果。我们已经创建了一个命令函数 ResolveXPath,它将 XPath 作为输入参数并返回结果。XPath 之美在于它易于管理变更。如果您创建了一个复杂的数据层次结构,XPath 也不会让您失望。现在我们已经具备了创建模拟层的所有必要元素,这不再是什么难事了。

块 - 增加复杂性

到目前为止,我们已经处理了非常简单的交易集,它们返回简单值,例如余额查询,它返回可用余额。在现实世界中,存在更复杂的交易,它们返回不同集合的列表,例如客户账户查询,其中一个客户可以有多个账户,并且每个账户都有不同的值。例如,一种账户类型可以是储蓄账户,另一种账户类型可以是贷款账户,等等。管理这些交易集需要一种不同的方法。

Block 字段在这些情况下对我们有帮助。Block 字段本身也是一个字段,它对 XPath 表达式进行求值,并根据 XPath 返回的对象数量重复所有子字段标签。让我们深入了解一下。

<Field Name="Block" 
       EvalFunction="ResolveBlock('//Customers/Customer[@PAN={0}]/Account',GetMessageFieldByIndex(5));" 
       DefaultValue="">

  <Field Name="AccountNumber" 
         EvalFunction="ResolveXPath('//Customers/Customer[@PAN={0}]/Account[@Id={1}]/@Number',GetMessageFieldByIndex(5), ContextId);" 
         DefaultValue=""/>
  <Field Name="AccountTitle" 
         EvalFunction="ResolveXPath('//Customers/Customer[@PAN={0}]/Account[@Id={1}]/@AccountTitle',GetMessageFieldByIndex(5), ContextId);" 
         DefaultValue=""/>
</Field>

在上面的示例中,Block 字段的 evalFunction 是特殊的,因为它包含 ResolveBlock 命令函数。此函数对 XPath 表达式进行求值,并将检索到的对象存储在 ArrayList 中。在上面的情况下,将为与提供的 CNIC 匹配的特定客户获取所有账户数据。此账户信息现在存储在 ArrayList 中,因为它将进一步用于后续子字段的求值,例如对于 AccountNumber 字段,eval 函数现在接受两个参数 [CNIC 和 Account Id]。第二个参数 [Account Id] 是唯一区分特定客户账户的参数,一旦识别出账户,获取账户号码就不是难事了,正如 XPath 中所述。

我们上面看到的是一个名为 Get Account Inquiry 的新交易。此交易返回多个账户信息以及各个账户的详细信息,例如账户号码和账户名称,使用 Block 字段。

Using the Code

模拟层应用程序设计非常简单,甚至足够简单地编写伪代码。

Function Process(string rawMessage, XmlDocument metaXML) 
{
    string[] splittedMessageArray = split rawMessage using ‘~’ as delimeter;
    Dictionary<string, string> request = new Dictionary<string, string>();
    For each field f in Header and Request of MetaXml Document
        If f.DefaultValue is not empty Then
           Value = f.DefaultValue
        Else
           EvaluateFunction = f.EvalFunction;
           Value = Evaluate(EvaluateFunction, splittedMessageArray, metaXML)
        End IF
        Request[f.Name] = Value;
    End For

    Arraylist responseFieldList = new ArrayList();

    For each field f in Header and Response of MetaXml Document
        If f.DefaultValue is not empty Then
           Value = f.DefaultValue
        Else
           EvaluateFunction = f.EvalFunction;
           Value = Evaluate(EvaluateFunction, splittedMessageArray, metaXML)
        End IF
        responseFieldList.Add(Value);
    End For
    String outMessage = string.join(‘~’, responseFieldList);
    Byte[] outByte = Encoding.ASCII.GetBytes(fourByteSize);

    return outByte;
}

这是一个三步过程:拆分接收到的消息,通过读取交易的标头和字段来构建请求映射,然后读取响应标头和字段,并使用源消息数组和 xpath 填充值来创建响应数组。就是这样,没有什么高深莫测的。

在代码中,Message Complexity 由 MessageManager 类管理。类似地,Transaction Meta Info 由 MetaInfoManager 管理,它是一个单例类,读取文件并将 XML 内容存储在内存中。

TCP 通信由下载的开源类 TCPServer3 管理,我已对其进行了一些调整以满足我的特定需求。

关注点

这里有趣的方法是,XPath 驱动的灵活性非常强大。可以在数小时内构建一个完整的模拟层,并且它非常易于更改。通过添加/删除字段或更改 XPath,可以在几分钟内修改一个交易。

历史

版本 1.0

© . All rights reserved.