【转发】农行银企直联XML对接socket SAP EPIC
前段时间项目中接入了农行的银企直联来完成代发的功能,当我拿到银行方面给过来的文档和资料后,发现和招行的银企直联模式差不多,大概就是:在window机器上开一个类似于前置机的小程序,作为我们和银行服务器直联数据连接的中介,我们发送xml数据给前置机,前置机再将数据加密后发送给银行服务器。但是万万没想到农行这个银企直联给我搞了不小的麻烦,他们的文档写的简直是不忍直视,接口返回码也模糊不清,没有明确说明。现在我把踩过的坑给分享一下。
准备开始
在开始之前我们会拿到2个东西,一个是中国农业银行银企通平台(4.70版).msi安装包,另一个是现金管理银企直连接入开发手册V1.2.1.wps接口文档说明
开始安装前置机程序,完成之后是这个样子:
测试用的客户号、操作员代码、操作员密码都会一并提供过来。
注意:正式环境下是需要插入一个key宝,由于现在是测试环境,在安装目录的etc路径下,用记事本编辑 etc\LoginSet.xml,将 IsKey 节点中的内容改成0,就可以不用KEY登录了。
系统设置
点击系统设置,在里面配置我们要用的模式:ERP公网接入、本地服务器地址、通讯协议、监听的端口等等。
直接上几张图吧:
注意:这里有一个坑需要说明一下:农行的这个程序是不支持http 协议(虽然他上面写着可以选择,无语)所以我们要用tcp协议。
组装XML数据
看到上图,已经成功的监听到了15999端口,现在我们就要向前置机所在的机器的ip+port的这个URL上推送XML数据,例如:192.168.1.111:15999
这里举一个范例:汇兑-单笔对似,xml数据报文是这样要求的。
java代码实例:
1 //5.汇兑-单笔对私 2 ApRoot root = new ApRoot(); 3 root.setCCTransCode("CFRT21"); 4 root.setAmt("9.10"); 5 builder.setRoot(root); 6 ApCmp cmp = new ApCmp(); 7 cmp.setDbAccNo("361101040010679"); 8 cmp.setDbProv("05"); 9 cmp.setDbCur("01"); 10 cmp.setDbLogAccNo(""); 11 cmp.setCrAccNo("6228453296002816764"); 12 cmp.setCrProv(""); 13 cmp.setCrCur("01"); 14 cmp.setCrLogAccNo(""); 15 cmp.setConFlag("1"); 16 builder.setCmp(cmp); 17 ApCorp corp = new ApCorp(); 18 corp.setPsFlag(""); 19 corp.setBookingFlag("0"); 20 corp.setBookingDate(""); 21 corp.setBookingTime(""); 22 corp.setUrgencyFlag("0"); 23 corp.setOthBankFlag("0"); 24 corp.setCrAccName("郑春广"); 25 corp.setDbAccName("内猛关仪太彩悟佑慊古丝"); 26 corp.setWhyUse("测试"); 27 corp.setPostscript("测试"); 28 builder.setCorp(corp);
安装要求组装数据:
1 1 2 String s = builder.toXmlString(builder); 3 /** 4 * 请求数据:加密标识(1加密,0不加密) + 请求xml数据的长度(默认7位,不够补空格) + 请求的xml 5 * @param s 请求的xml 6 * @author jieYW 7 * @date 2018/5/29 8 * @return java.lang.String 9 */ 10 public String genRequestData(String s)throws Exception{ 11 return "1" + String.format("%1$-6s", s.getBytes("gbk").length) + s; 12 }
跟据交易明细表 与ERP对接生成收款单(这里主要说报文提交与读取)
1 #region 农行接口通讯方法 2 private static Socket ConnectSocket(string server, int port) 3 { 4 Socket s = null; 5 IPEndPoint ipe = new IPEndPoint(IPAddress.Parse(server), port); 6 Socket tempSocket = new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp); 7 tempSocket.Connect(ipe); 8 if (tempSocket.Connected) 9 { 10 s = tempSocket; 11 } 12 return s; 13 } 14 /// <summary> 15 /// 通讯发送报文 16 /// </summary> 17 /// <param name="server">服务ip 这里是连接农行客户端 所以ip默认应该是127.0.0.1</param> 18 /// <param name="port">端口 这里是连接农行客户端,客户端登录后系统设置里会设置端口</param> 19 /// <param name="DbAccNo">查询的账户银行账号</param> 20 /// <param name="startTime">末笔时间戳 (文档有说明,通过这个查过的数据就不再过滤出来)</param> 21 /// <param name="StarDate">查询的起始日期</param> 22 /// <param name="EndDate">查询的截止日期</param> 23 /// <returns></returns> 24 private string SocketSendReceive(string server, int port, string DbAccNo,string startTime,DateTime StarDate,DateTime EndDate) 25 { 26 string _head = "<ap><CCTransCode>CQRA10</CCTransCode><ProductID>ICC</ProductID><ChannelType>ERP</ChannelType><CorpNo></CorpNo><OpNo></OpNo><AuthNo></AuthNo><ReqSeqNo></ReqSeqNo><ReqDate>" + DateTime.Today.ToShortDateString() + "</ReqDate><ReqTime>" + DateTime.Now.ToShortTimeString() + "</ReqTime><Sign></Sign>"; 27 string request = _head + "<CCTransCode>CQRA10</CCTransCode><Corp><StartDate>" + StarDate.ToString("yyyyMMdd") + "</StartDate><EndDate>" + EndDate.ToString("yyyyMMdd") + "</EndDate></Corp><Channel><LastJrnNo></LastJrnNo></Channel><Cmp><DbAccNo>" + DbAccNo + "</DbAccNo><DbProv>38</DbProv><DbCur>01</DbCur><StartTime>" + startTime + "</StartTime></Cmp></ap>"; 28 Byte[] byl = Encoding.Default.GetBytes(request); 29 string _len = "1" + byl.Length.ToString().PadRight(6, ' ');//根据文档说明 报文前面要有7位数字,第一位是加密否标示,后面6位是报文的长度 30 Byte[] bytesSent = Encoding.Default.GetBytes(_len + request); 31 Byte[] bytesReceived = new Byte[256]; 32 // Create a socket connection with the specified server and port. 33 Socket s = ConnectSocket(server, port); 34 if (s == null) 35 throw new Exception("Error:通讯连接失败!请检查农行客户端是否登录!"); 36 37 // Send request to the server. 38 s.Send(bytesSent, bytesSent.Length, 0); 39 // Receive the server home page content. 40 int bytes = 0; 41 string page = ""; 42 // The following will block until te page is transmitted. 43 do 44 { 45 bytes = s.Receive(bytesReceived, bytesReceived.Length, 0); 46 page = page + Encoding.Default.GetString(bytesReceived, 0, bytes); 47 } 48 while (bytes > 0); 49 return page; 50 } 51 #endregion
Socket接收端代码:
1 string result = SocketSendReceive(host, port, idcode, strattime, _starDate, _endDate); 2 XmlDocument _xml = new XmlDocument(); 3 _xml.LoadXml(result.Substring(6)); 4 XmlNode _RespSource = _xml.SelectSingleNode("//RespSource"); 5 if (_RespSource.InnerText != "0") 6 { 7 XmlNode _RespInfo = _xml.SelectSingleNode("//RespInfo");//返回的提示信息 8 XmlNode _RxtInfo = _xml.SelectSingleNode("//RxtInfo");//返回的提示信息 9 //////报错信息记录/// 10 } 11 else 12 { 13 XmlNode _xnfilename = _xml.SelectSingleNode("//BatchFileName");//返回数据以文件形式保存 14 string _data = System.IO.File.ReadAllText("C:/Program Files (x86)/中国农业银行/中国农业银行银企通平台/detail/" + _xnfilename.InnerText, Encoding.Default);//银行客户端的文件路径 15 string[] _strdata = _data.Split('\n'); 16 foreach (string _da in _strdata) 17 { 18 if (!string.IsNullOrEmpty(_da)) 19 { 20 string[] _strfi = _da.Split('|'); 21 decimal _amtn = Convert.ToDecimal(_strfi[18].ToString()); 22 if (_amtn > 0)//只取收款 23 { 24 string _codeno = _strfi[11].ToString();//对方银行账户 25 string _compname = _strfi[13].ToString();//对方户名 26 //查询客户账户资料 27 DataSet _dscust = _query.DoSQLString("select CUS_NO from cust where NAME='" + _compname + "'");//_query 为底层查数据方法 28 if (_dscust.Tables[0].Rows.Count > 0) 29 { 30 string _lastTime = _strfi[4].ToString(); ;//末笔时间戳; 31 string _rem = _strfi[31].ToString();//摘要 32 ///////////////保存收款单/////////////////////// 33 ///////////////记录末笔时间戳/////////////////////// 34 } 35 else 36 { 37 //////报错信息记录/// 38 } 39 } 40 } 41 } 42 }
注意:这里也有一个大坑,字符编码必须通过gbk的编码。前置机内部是通过gbk来解码的。这里如果不设置gbk,你在报文中有汉字的时候就gg了,会一直报这个错误:接收请求报文失败 -接收报体失败 - POLL失败退出 - 偏移量 = 750,当时这个问题困扰了我一段时间,因为我是知道他字符编码是gbk的,所以我只在socket的输出流里设置了字符的编码,经过血一般经历后终于想到了这里,然后彻底解决中文乱码的问题。
【讲xml转换成socket套接字段】
socket发送数据
将上述组装好的数据通过tcp协议发送给前置机:
1 String s = builder.toXmlString(builder); 2 ApHttpRequest request = new ApHttpRequest(); 3 String s1 = request.socketSendAndReceive("192.168.1.111", 15999, builder.genRequestData(s)); 4 System.out.println("接受数据:" + s1);
测试结果:
补充代码
XML组装:
1 /** 2 * 将ApXmlBuilder格式的数据转化为xml形式 3 * 4 * @param builder 5 * @author jieYW 6 * @date 2018/5/29 7 * @return java.lang.String 8 */ 9 public String toXmlString(ApXmlBuilder builder)throws Exception{ 10 StringBuffer sb = new StringBuffer("<ap>"); 11 Field[] fields = builder.getClass().getDeclaredFields(); 12 for (int i = 0; i < fields.length; i++) { 13 Field field = fields[i]; 14 field.setAccessible(true); 15 Object item = field.get(builder); 16 if(item == null){ 17 continue; 18 } 19 String name = DaoSupport.toFirstUpperCase(field.getName()); 20 if(!name.equals("Root")){ 21 sb.append("<" + name + ">"); 22 } 23 Field[] itemFields = item.getClass().getDeclaredFields(); 24 for (int j = 0; j < itemFields.length; j++) { 25 Field itemField = itemFields[j]; 26 itemField.setAccessible(true); 27 Object itemObject = itemField.get(item); 28 if(itemObject == null){ 29 continue; 30 } 31 String itemFieldName = itemField.getName(); 32 sb.append("<" + itemFieldName + ">"); 33 sb.append(itemField.get(item).toString()); 34 sb.append("</" + itemFieldName + ">"); 35 } 36 if(!name.equals("Root")){ 37 sb.append("</" + name + ">"); 38 } 39 } 40 sb.append("</ap>"); 41 return sb.toString(); 42 } 43 ————————————————
发送报文:
1 public String socketSendAndReceive(String url, int port,String data)throws Exception{ 2 System.out.println("请求数据:" + data); 3 Socket socket = new Socket(url, port); 4 OutputStream bw = socket.getOutputStream(); 5 bw.write(data.getBytes("gbk")); 6 bw.flush(); 7 InputStream ips = socket.getInputStream(); 8 StringBuffer sb = new StringBuffer(); 9 int len = 0; 10 byte[] buf = new byte[1024]; 11 while((len=ips.read(buf))!=-1){ 12 sb.append(new String(buf,0,len,"gbk")); 13 } 14 bw.close(); 15 ips.close(); 16 socket.close(); 17 return sb.toString(); 18 }
解析XML:
1 /** 2 * 将xml数据解析为ApXmlBuilde格式的数据 3 * 4 * @param msg 5 * @author jieYW 6 * @date 2018/5/29 7 * @return com.mind.pay.abc.ap.ApXmlBuilder 8 */ 9 public static ApXmlBuilder parseXml(String msg)throws Exception { 10 ApXmlBuilder builder = new ApXmlBuilder(); 11 Method[] methods = builder.getClass().getMethods(); 12 ApRoot apRoot = new ApRoot(); 13 Method[] rootMethods = apRoot.getClass().getMethods(); 14 15 Map<String,Method> methodMap = new HashMap<>(); 16 for (Method method : methods) { 17 methodMap.put(method.getName(),method); 18 } 19 InputStream inputStream = new ByteArrayInputStream(msg.getBytes("UTF-8")); 20 SAXReader reader = new SAXReader(); 21 Document document = reader.read(inputStream); 22 Element root = document.getRootElement(); 23 List<Element> elementList = root.elements(); 24 // 遍历所有子节点 25 for (Element e : elementList){ 26 if(methodMap.keySet().contains("set" + e.getName())){ 27 Class<?> aClass = Class.forName("com.mind.pay.abc.ap.Ap" + e.getName()); 28 Object itemObject = aClass.newInstance(); 29 List<Element> items = e.elements(); 30 for (Element itemElement : items) { 31 Method[] itemMethods = itemObject.getClass().getMethods(); 32 for (Method itemMethod : itemMethods) { 33 //如果字段存在,invoke方法赋值 34 if(itemMethod.getName().contains("set"+itemElement.getName())){ 35 itemMethod.invoke(itemObject,itemElement.getText()); 36 } 37 } 38 } 39 methodMap.get("set" + e.getName()).invoke(builder,itemObject); 40 }else{ 41 //根目录下的参数,封装到apRoot中 42 for (Method rootMethod : rootMethods) { 43 //如果字段存在,invoke方法赋值 44 if(rootMethod.getName().contains("set"+e.getName())){ 45 rootMethod.invoke(apRoot,e.getText()); 46 } 47 } 48 builder.setRoot(apRoot); 49 } 50 } 51 // 释放资源 52 inputStream.close(); 53 inputStream = null; 54 return builder; 55 }
*******术语解释********
1.网银和银企直联的区别
1.1网银
网银,简单地讲,就是银行在互联网上开展的各种业务,也就是银行客户利用个人计算机通过Internet获得银行的各项服务,银行利用专用的服务器提供各项在线服务。对银行,这种高效、全天候的服务能够吸引更多的用户,而且网上银行本身可以大大削减现有银行众多的分支机构,减少工作人员,提高工作效率。
1.2.银企直联
银企直联是一种新的网上企业银行系统与企业的财务软件系统在线直接联接的接入方式。银企直联通过因特网或专线连接方式,实现了银行和企业计算机系统的有机融合和平滑对接。企业通过财务系统的界面就可直接完成对银行账户以及资金的管理和调度,进行信息查询、转账支付等各项业务操作。同时,银企直联可以为企业在其财务系统中开发和定制个性化功能提供支持,具有信息同步、高效简便、个性服务和安全可靠的鲜明特色。银企直联能够做到与企业计算机系统的对接,方便的完成企业系统的与银行有关的交易。
通过了解到网银和银企直联的相关知识,可以看出网银和银企直联都是可以完成企业的银行业务,不同的是,网银不能够整合企业系统,不能够有机的与企业系统进行对接,所以用户需要登陆银行的网银系统,进行手工数据的录入或导入导出。银企直联克服了这种弊端,银企直联可以与企业系统对接,可以将企业的业务系统与银行的系统虚拟的联结在一起,无缝的完成企业的各种银行业务。另外,通过银企直联,企业可以搭建统一平台,而网银做不到。
2 专业术语
2.1 接口IP地址种类
银行的服务IP,主要是银行内部对银企通服务的IP地址,此IP对于银行来说是确定的,不会随客户的改变而改变。此IP主要用于企业前置机的银行服务程序的接入端,银行对每一个客户安装的企业客户端程序,都需要连接此IP地址。
前置机的服务IP,主要是针对企业客户端的接入地址,此IP地址可以随客户的不同网络进行调整,其服务端口也可依照客户前置机的不同而进行调整。前置机的银行服务程序主要负责数据的加密、验证和转发功能。银行客户端程序连接到银行服务IP,并且对企业程序提供数据的接收和发送功能。所以前置机的银行服务程序一般需要配置两个IP地址,一个是银行的服务IP地址,另一个是对企业提供服务的IP和端口。通常对企业提供服务的IP也是该程序所在前置机的IP地址。
接口的通讯IP,主要是用户企业客户端连接前置机的IP,此IP通常为银行服务程序的IP。
2.2 报文
所谓报文,就算数据交换双方所共识的一种文本格式,报文作为数据交换是非常重要的,在日常数据交换的过程中,报文必须正确,而且必须与约定的相同。否则,对方将无法明白所收到数据的明确意图。
2.2.1 报文种类
目前接口经常使用的报文主要包括一下几种:
XML报文:所谓XML,也是一种目前国际标准的文档格式,XML报文的主要优点在于信息明确,便于阅读和理解,而且对于每个字段,其长度也可随时调整。
定长串:此类报文格式主要是通过约定一个长度的信息作为预定的内容,将需要传输的数据通过固定的长度发送给对方,由于长度双方都有约定,所以对方收到此数据后,即可知道固定长度的信息内容。
固定顺序加分隔符(多域串):此报文格式一般需要约定报文的字段的顺序,然后传输数据的各类信息通过分隔符的形式分开,用户通过分析数据分隔符来判定信息的位置,完成数据的传输。
注:以上介绍了几种常用的报文格式,在日常使用过程中,可以将以上种类的报文进行融合,但是一般XML内部可以嵌套使用定长串或者多域串。
2.3 通讯方式
OLE方式:此类方式一般由银行提供OLE开发控件和调用说明,客户端安装和注册这些控件,按照银行提供的调用说明进行数据的交互,数据的加密、发送、接收和解密。
SOCK通讯:此类方式首选需要银行提供服务的IP地址和端口,客户端将约定的交易报文发送到此IP地址和端口。然后接收银行返回的数据。
2.4 银行签约
在使用银企直联时,相当于在集团内部有一个银行的柜员,所有发生的交易都由该柜员完成,所谓柜员,就是银行对集团用户的唯一标识号码,有的银行为客户号,有的银行叫做操作员,这些都是银行受理交易的一个接入要素。因为集团所有的帐号都由该柜员管理,所以办理银行签约,就相当于给该柜员(客户号、操作员)于集团的帐号建立操作权限。签约完成后,需要银行根据签约的具体内容进行操作员帐号的初始化工作。完成这些功能,企业就可以以该柜员的身份操作签约的帐号,完成日常的交易。
2.5 签约模式
目前银行的签约模式主要分类两类,一类为收支分开模式,此类签约模式在银行内部定义了各个帐号的收入类帐号和支出类帐号。此类签约的签约帐号所发生的交易必须严格按照银行内部定义的收支方向进行资金划转交易。另外一类为统一收支模式,此类签约模式在银行内部,签约帐号之间的收支不具备方向性。任何帐号之间都可以进行资金的划转。在对外支付时,只要支付帐号具有对外支付的权限,就可以进行对外支付交易。
2.6 资金上划
资金上划就是集团内部发生的,集团总帐户收取下属单位的帐户的资金,如果该集团为收支分开模式,则收取的为收入类帐户的资金。如果为统一收支模式,则收取的为该总帐号所有允许划款的子帐户的资金。
2.7 资金下拨
资金下拨就是集团内部发生的,集团总帐户支付给下属单位的划转交易。
2.8 调拨策略
调拨策略即用户指定的、具有一定特性的资金划转交易的交易规则,使用调拨策略,接口可自动跟据调拨策略的功能完成资金的划转交易,使用调拨策略的帐户,只限于集团内部签约帐号。
2.9 联动下拨
联动下拨是指在对外付款的交易发生时,由于子帐户付款,而该子帐户的帐户余额又为零,此时会由接口自动从总帐户向该子帐户进行拨款,使该子帐户的余额满足其支付的金额
http转socket的代码
————————————————
参考1:
原文链接:https://blog.csdn.net/Chinook/article/details/5635900
参考2:
原文链接:https://blog.csdn.net/wei389083222/article/details/80802839
参考3:
https://blog.csdn.net/TangHao_0226/article/details/80278576
Http协议对接Socket服务(TCP协议)
当我们在浏览器地址栏输入对应的IP地址,其实也就是浏览器创建了一个socket连接。那么服务端能否响应一段文字呢?
1、我们先来创建一个Request
类,用于处于http的请求。
Request.java:
这个Request类有2个方法:构造方法接受一个InputStream对象
(socket中的输入流对象),readHtml()
方法是用来读去到底浏览器给我们发送了什么内容(从inputStream对象中获取内容)。
2、然后我们就可以在socket服务端使用Request类
了
MyServer.java:
3、运行MyServer.java,也就是运行socket服务端。然后浏览器访问http://localhost:9000
。
我们在服务端打印了Request对象
读取到的内容,我们可以在控制台看见:
这就是一个http发送请求的数据。
4、服务端能够获取到浏览器的输入了,我们尝试给浏览器一个响应,比如响应一个字符串this is server
。
我创建一个http响应类Response.java
:
可以看出writeHtml()
方法中,拼装了一个符合http协议响应格式的字符串(符合http协议才会被浏览器正常解析识别)。
5、来到我们的socket服务端代码,现在既要处理输入也要处理输出了
MyServer.java:
现在浏览器访问http://localhost:9000
的话,页上给我们显示this is server
这样一个字符串了。
银企上线后的常见问题:
(1).生产环境应用服务器连接不上前置机服务器
运维人员对于连接前置机的服务器IP进行了限制;另外,还需要财务人员远程登录前置机服务器,开启前置机;
(2).返回错误信息:有审批业务,不可直接经办
(3).行内支付使用直接支付接口,返回错误信息:收方必须为商户账户
咨询招行技术人员得知是因为我们公司在招行的账户受到限制,收款方必须加入白名单才能打款。询问财务人员,确实如此,经加入白名单后,打款成功。
跨行未受到限制,估计是因为走的是央行的超级网银渠道,所以没有限制。
(4).跨行支付收款方行号的问题
跨行支付时需要输入收款行行号,只能输入总行的行号,输入支行的行号报错。商务人员会提供“人行网银互联(跨行清算系统)联行号信息.txt”,其中有各银行及对应的联行号。
本文来自博客园,作者:Slashout,转载请注明原文链接:https://www.cnblogs.com/SlashOut/p/12994725.html