前段時間項目中接入了農行的銀企直聯來完成代發的功能,當我拿到銀行方面給過來的文檔和資料后,發現和招行的銀企直聯模式差不多,大概就是:在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”,其中有各銀行及對應的聯行號。