1. 程式人生 > >Mr.Fang出品:銀企互聯(NC模式)開發者版本(.NET WebService中介軟體,Java、PHP、Python等跨語言測試通過)

Mr.Fang出品:銀企互聯(NC模式)開發者版本(.NET WebService中介軟體,Java、PHP、Python等跨語言測試通過)

本開發者版本特點:

  1. ERP系統只需要通過HTTP協議將引數和傳送內容一次性POST到WebService指定頁面,接收應答即可。開發者不需要關注簽名、提交頁面格式、報文頭引數、壓縮模式等,實現了跨平臺、跨語言的簡單開發,技術門檻降至極低。
  2. 支援壓縮模式提交大批量指令:rd最外層頭尾套<zip>、</zip>即可,比如:……<zip><rd>……</rd>……<rd>……</rd></zip>……。WebService會自行壓縮資料。如將壓縮好的資料放入zip節點(官方做法)後使用本程式提交反而會出錯。
  3. XML報文中日期/時間關鍵引數自動配置,大部分情況下自適應銀行主機時間。特別是在測試環境的時候,不需要調整本機時間。
  4. ICBC_Log資料夾可以儲存日誌檔案,方便跟蹤。
  5. IIS部署方便,幾分鐘搞定。
  6. 提供PDF電子回單等高階應用。

說明:與第一篇文章類似,調整一下格式,補充一些內容。

  1. 適用物件:自己動手開發銀企互聯的企業客戶,能與ERP系統對接,故稱為開發者版本。
  2. 非官方產品。
  3. 開發者免責:開發者力求正確,也經過測試,但無法避免潛在的錯誤。
  4. 客戶使用銀企互聯,應遵從謹小慎微的原則,從查詢業務、小金額業務著手,必要時採用銀企互聯提交指令、企業網銀授權的模式。
  5. 本WebService程式在Windows作業系統上部署,需要.net4.0框架。
  6. 未盡事宜,請參閱官方開發手冊。

常規開發模式中,資料互動方式如下:

如果是簽名類交易,業務發起流程如上圖。客戶開發的工作量主要是 1、3,較為繁瑣。 如果是查詢類交易,則上述步驟中 1、2 不做。

企業客戶開發過程通常遇到的問題:

  1. 不太願意仔細看官方開發文件。實際上,我自己開始著手練習銀企互聯技術,就是按照官方文件來的。
  2. 不知道如何去做簽名。
  3. 銀行測試環境日期非標準,無法靈活應對請求資訊中的日期、時間相關引數。有時候,正式的環境也經常發生erp伺服器時間比標準時間誤差大的情況,導致交易直接被銀行拒絕。
  4. 很少自己產生日誌檔案,遇到錯誤難以確定。向銀行陳述錯誤沒有依據。
  5. 採用過程化,很少物件化,程式碼複用性差,程式不健壯。
  6. 官方提供的DEMO比較少。
  7. 部分企業客戶的系統不支援socket協議,需要銀行或者開發商提供輔助手段。
  8. 一些高階技巧、應用無從談起,比如大批量指令傳送,製作PDF格式的電子回單。

本方案設計的資料互動為:

本方案圖中步驟 2、5,相當於第一圖中的 1、2、3、6,由 WebService 自動執行。

部署本WebService,需要.NET4框架。

  1. 如果沒有按照.net4框架,請安裝。如果沒有安裝IIS,請安裝。為減少錯誤,其中應用程式開發功能請全選。
  2. 新增網站,選擇物理路徑,自己定義一個不常用的埠,本例使用1398。
  3. 執行。瀏覽器開啟對應地址,比如http://127.0.0.1:1398,點選WebService.asmx,可以看到提供的服務。點選CheckNC或者CheckWS,可以檢視到對應返回內容,則表明部署成功。
  4. 如果需要從區域網其他電腦訪問本WebService,則需要在防火牆中新增的本服務埠(入站、TCP)。

WebService中Web.Config資訊:

nc_ip:NetSafeClient的IP地址。

nc_hp、nc_sp:NetSafeClient的HTTPS服務埠以及簽名埠。

cis、id:客戶企業網銀對應的CIS編號和證書名稱。使用ICBC_YQHL方法時與XML中資訊校驗。

log、pdf_save:ICBC_YQHL方法是否產生日誌檔案,是否保留pdf回單文件。日誌檔案是跟蹤錯誤,向銀行陳述錯誤的重要依據。雖然NetSafeClient本身可以產生日誌檔案,但NC2.0版本不建議長期開通-debugmax除錯模式,很容易因日誌檔案過大而宕機;NC3.0屬於迴圈存放日誌,但發生錯誤後,要在10個日誌檔案中(每個可能10M大小)中查詢具體資訊,也是費時費力的。本軟體的日誌是每次呼叫介面產生一個日誌,需要時能迅速定位。

如果防火牆埠未新增,從其他電腦訪問出錯:

新增埠後,訪問正常:

提供的方法以及需要的引數,可以自行點選WebService.asmx檢視。比如QACCBAL,從圖中看到就是acct一個引數:

本專案的第一個測試客戶,是因為他們的sap不支援socket協議。給了他們第一個版本(引數個數和現在有些不同),經過除錯,暢通。下圖是客戶sap介面,他們用了get方式,已經建議修改為post。

對於最重要的ICBC_YQHL方法做些說明:

  • 程式自動替換內容的空白節點請使用<node></node>這種格式,如果寫成<node/>這種,程式無法自動填充其數值。TranDate、TranTime、SignTime等欄位,程式將自動補充,為避免部分系統自動將<TranDate></TranDate>變成<TranDate/>這種格式,可以隨意輸入些內容,比如<TranDate>NotCare</TranDate>。
  • fSeqno節點內容如果空白或內容長度<=3,則由系統自動填寫。個人建議:查詢類的報文該欄位可以空白或輸入長度小於3的內容,交易類的請指定fseqno內容(長度>3),避免傳送請求後沒有接收到回執而無從判斷銀行究竟接收到了指令沒有。

從網上摘錄一些程式碼,作為本專案的多語言測試。

PHP:先查詢餘額;做一筆支付;再查詢餘額。由於是工行系統內支付,實時處理,故第二次查詢的時候,餘額已經變化了。

<?php
header('charset: GBK');

function send_post($url, $post_data) {
    $postdata = http_build_query($post_data);
    $options = array(
        'http' => array(
            'method' => 'POST',
            'header' => 'Content-type:application/x-www-form-urlencoded',
            'content' => $postdata,
            'timeout' => 30 // 超時時間(單位:s)
        )
    );
    $context = stream_context_create($options);
    $result = file_get_contents($url, false, $context);
    return $result;
}
$xml_info='<?xml version="1.0"encoding="GBK"?><CMS><eb><pub><TransCode>PAYENT</TransCode><CIS>46694306-XAAAAA</CIS>'
.'<BankCode>102</BankCode><ID>suzhouTest.y.1102</ID><TranDate></TranDate>NotCare<TranTime>NotCare</TranTime><fSeqno>X</fSeqno>'
.'</pub><in><OnlBatF>1</OnlBatF><SettleMode>0</SettleMode><TotalNum>1</TotalNum><TotalAmt>1122</TotalAmt><SignTime></SignTime>'
.'<ReqReserved1></ReqReserved1><ReqReserved2></ReqReserved2><rd><iSeqno>1</iSeqno><ReimburseNo></ReimburseNo><ReimburseNum>'
.'</ReimburseNum><StartDate></StartDate><StartTime></StartTime><PayType>1</PayType><PayAccNo>1102020109000009078</PayAccNo>'
.'<PayAccNameCN>剝濫火判酬</PayAccNameCN><PayAccNameEN></PayAccNameEN><RecAccNo>1102020109000203242</RecAccNo>'
.'<RecAccNameCN>剝濫判幕耕婚舒憾</RecAccNameCN><RecAccNameEN></RecAccNameEN><SysIOFlg>1</SysIOFlg><IsSameCity></IsSameCity>'
.'<Prop></Prop><RecICBCCode></RecICBCCode><RecCityName>工行系統內無需註明</RecCityName><RecBankNo></RecBankNo>'
.'<RecBankName>工行系統內無需註明</RecBankName><CurrType>001</CurrType><PayAmt>1122</PayAmt><UseCode></UseCode>'
.'<UseCN>上線測試</UseCN><EnSummary></EnSummary><PostScript></PostScript><Summary></Summary><Ref></Ref><Oref></Oref>'
.'<ERPSqn></ERPSqn><BusCode></BusCode><ERPcheckno></ERPcheckno><CrvouhType></CrvouhType><CrvouhName></CrvouhName>'
.'<CrvouhNo></CrvouhNo><BankType></BankType><FileNames></FileNames><Indexs></Indexs><PaySubNo></PaySubNo>'
.'<RecSubNo></RecSubNo><MCardNo></MCardNo><MCardName></MCardName></rd></in></eb></CMS>';
$post_data = array(
	'ver'=>'0.0.1.0',
	'b64_xml'=>base64_encode($xml_info)
);
$post_data2 = array('acct'=>'1102020109000009078');
echo send_post('http://127.0.0.1:1398/WebService.asmx/QACCBAL', $post_data2);
echo "\r\n";
echo send_post('http://127.0.0.1:1398/WebService.asmx/ICBC_YQHL', $post_data);
echo "\r\n";
echo send_post('http://127.0.0.1:1398/WebService.asmx/QACCBAL', $post_data2);

?>

對ReqResult資訊做BASE64解碼,就能得到原文了,在此不述。

Python3.7:相同的演示。

#!/usr/bin/python
# -*- coding: GBK -*-
import base64
from urllib import request
from urllib import parse
from urllib.request import urlopen

test_data = {'acct':'1102020109000009078'}
test_data_urlencode = parse.urlencode(test_data).encode('GBK')
requrl = 'http://10.0.0.5:1398/WebService.asmx/QACCBAL'
req = request.Request(requrl,test_data_urlencode)
res_data = urlopen(req)
res = res_data.read()
print (res.decode())

xml_info='<?xml version="1.0"encoding="GBK"?><CMS><eb><pub><TransCode>PAYENT</TransCode><CIS>46694306-XAAAAA</CIS>'\
'<BankCode>102</BankCode><ID>suzhouTest.y.1102</ID><TranDate></TranDate>NotCare<TranTime>NotCare</TranTime><fSeqno>X</fSeqno>'\
'</pub><in><OnlBatF>1</OnlBatF><SettleMode>0</SettleMode><TotalNum>1</TotalNum><TotalAmt>4455</TotalAmt><SignTime></SignTime>'\
'<ReqReserved1></ReqReserved1><ReqReserved2></ReqReserved2><rd><iSeqno>1</iSeqno><ReimburseNo></ReimburseNo><ReimburseNum>'\
'</ReimburseNum><StartDate></StartDate><StartTime></StartTime><PayType>1</PayType><PayAccNo>1102020109000009078</PayAccNo>'\
'<PayAccNameCN>剝濫火判酬</PayAccNameCN><PayAccNameEN></PayAccNameEN><RecAccNo>1102020109000203242</RecAccNo>'\
'<RecAccNameCN>剝濫判幕耕婚舒憾</RecAccNameCN><RecAccNameEN></RecAccNameEN><SysIOFlg>1</SysIOFlg><IsSameCity></IsSameCity>'\
'<Prop></Prop><RecICBCCode></RecICBCCode><RecCityName>工行系統內無需註明</RecCityName><RecBankNo></RecBankNo>'\
'<RecBankName>工行系統內無需註明</RecBankName><CurrType>001</CurrType><PayAmt>4455</PayAmt><UseCode></UseCode>'\
'<UseCN>上線測試</UseCN><EnSummary></EnSummary><PostScript></PostScript><Summary></Summary><Ref></Ref><Oref></Oref>'\
'<ERPSqn></ERPSqn><BusCode></BusCode><ERPcheckno></ERPcheckno><CrvouhType></CrvouhType><CrvouhName></CrvouhName>'\
'<CrvouhNo></CrvouhNo><BankType></BankType><FileNames></FileNames><Indexs></Indexs><PaySubNo></PaySubNo>'\
'<RecSubNo></RecSubNo><MCardNo></MCardNo><MCardName></MCardName></rd></in></eb></CMS>'

test_data = {'ver':'0.0.1.0','b64_xml':base64.b64encode(xml_info.encode('GBK'))}
test_data_urlencode = parse.urlencode((test_data)).encode('GBK')
requrl = 'http://10.0.0.5:1398/WebService.asmx/ICBC_YQHL'
req = request.Request(requrl,test_data_urlencode)
res_data = urlopen(req)
res = res_data.read()
print (res.decode())

test_data = {'acct':'1102020109000009078'}
test_data_urlencode = parse.urlencode(test_data).encode('GBK')
requrl = 'http://10.0.0.5:1398/WebService.asmx/QACCBAL'
req = request.Request(requrl,test_data_urlencode)
res_data = urlopen(req)
res = res_data.read()
print (res.decode())

JAVA:

import java.util.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;

public class YQHL
{
public static void main(String[] args)throws Exception
{
 //Base64.Decoder decoder = Base64.getDecoder();
 Base64.Encoder encoder = Base64.getEncoder();
 String xml_text = "<?xml version=\"1.0\"encoding=\"GBK\"?><CMS><eb><pub><TransCode>PAYENT</TransCode><CIS>46694306-XAAAAA</CIS>"+
"<BankCode>102</BankCode><ID>suzhouTest.y.1102</ID><TranDate></TranDate>NotCare<TranTime>NotCare</TranTime><fSeqno>X</fSeqno>"+
"</pub><in><OnlBatF>1</OnlBatF><SettleMode>0</SettleMode><TotalNum>1</TotalNum><TotalAmt>6789</TotalAmt><SignTime></SignTime>"+
"<ReqReserved1></ReqReserved1><ReqReserved2></ReqReserved2><rd><iSeqno>1</iSeqno><ReimburseNo></ReimburseNo><ReimburseNum>"+
"</ReimburseNum><StartDate></StartDate><StartTime></StartTime><PayType>1</PayType><PayAccNo>1102020109000009078</PayAccNo>"+
"<PayAccNameCN>剝濫火判酬</PayAccNameCN><PayAccNameEN></PayAccNameEN><RecAccNo>1102020109000203242</RecAccNo>"+
"<RecAccNameCN>剝濫判幕耕婚舒憾</RecAccNameCN><RecAccNameEN></RecAccNameEN><SysIOFlg>1</SysIOFlg><IsSameCity></IsSameCity>"+
"<Prop></Prop><RecICBCCode></RecICBCCode><RecCityName>工行系統內無需註明</RecCityName><RecBankNo></RecBankNo>"+
"<RecBankName>工行系統內無需註明</RecBankName><CurrType>001</CurrType><PayAmt>6789</PayAmt><UseCode></UseCode>"+
"<UseCN>上線測試</UseCN><EnSummary></EnSummary><PostScript></PostScript><Summary></Summary><Ref></Ref><Oref></Oref>"+
"<ERPSqn></ERPSqn><BusCode></BusCode><ERPcheckno></ERPcheckno><CrvouhType></CrvouhType><CrvouhName></CrvouhName>"+
"<CrvouhNo></CrvouhNo><BankType></BankType><FileNames></FileNames><Indexs></Indexs><PaySubNo></PaySubNo>"+
"<RecSubNo></RecSubNo><MCardNo></MCardNo><MCardName></MCardName></rd></in></eb></CMS>";

 byte[] textByte = xml_text.getBytes("GBK");

 String encodedText = encoder.encodeToString(textByte);
//System.out.println(encodedText);

//System.out.println(new String(decoder.decode(encodedText), "GBK"));
System.out.println(sendPost("http://10.0.0.5:1398/WebService.asmx/QACCBAL","acct=1102020109000203242"));
System.out.println(sendPost("http://10.0.0.5:1398/WebService.asmx/ICBC_YQHL","ver=0.0.1.0&b64_xml="+encodedText));
System.out.println(sendPost("http://10.0.0.5:1398/WebService.asmx/QACCBAL","acct=1102020109000203242"));

}
 public static String sendPost(String url, String param) {
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        try {
            URL realUrl = new URL(url);
            // 開啟和URL之間的連線
            URLConnection conn = realUrl.openConnection();
            // 設定通用的請求屬性
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            conn.setRequestProperty("Accept-Charset", "utf-8");
            conn.setRequestProperty("contentType", "utf-8");
            // 傳送POST請求必須設定如下兩行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 獲取URLConnection物件對應的輸出流
            out = new PrintWriter(conn.getOutputStream());
            // 傳送請求引數
            out.print(param);
            // flush輸出流的緩衝
            out.flush();
            // 定義BufferedReader輸入流來讀取URL的響應
            in = new BufferedReader(
                    new InputStreamReader(conn.getInputStream(),"UTF-8"));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("傳送 POST 請求出現異常!"+e);
            e.printStackTrace();
        }
        //使用finally塊來關閉輸出流、輸入流
        finally{
            try{
                if(out!=null){
                    out.close();
                }
                if(in!=null){
                    in.close();
                }
            }
            catch(IOException ex){
                ex.printStackTrace();
            }
        }
        return result;
    } 
}

執行:

使用.NET:介紹一下大批量支付(一次性提交1000筆指令)。使用本專案會非常簡單,在最外層的rd迴圈節點套一個<zip>即可。

using System;
using System.Text;
using System.Net;
using System.IO;

class Program
 {
  static void Main(string[] args)
  {
	string rd_text="<rd><iSeqno>_X_</iSeqno><ReimburseNo></ReimburseNo><ReimburseNum></ReimburseNum><StartDate></StartDate><StartTime></StartTime>"+
"<PayType>1</PayType><PayAccNo>1102020109000009078</PayAccNo><PayAccNameCN>剝濫火判酬</PayAccNameCN><PayAccNameEN></PayAccNameEN>"+
"<RecAccNo>1102020109000203242</RecAccNo><RecAccNameCN>剝濫判幕耕婚舒憾</RecAccNameCN><RecAccNameEN></RecAccNameEN>"+
"<SysIOFlg>1</SysIOFlg><IsSameCity></IsSameCity><Prop></Prop><RecICBCCode></RecICBCCode><RecCityName>工行系統內無需註明"+
"</RecCityName><RecBankNo></RecBankNo><RecBankName>工行系統內無需註明</RecBankName><CurrType>001</CurrType><PayAmt>_X_</PayAmt>"+
"<UseCode></UseCode><UseCN>上線測試</UseCN><EnSummary></EnSummary><PostScript></PostScript><Summary></Summary><Ref></Ref><Oref></Oref>"+
"<ERPSqn></ERPSqn><BusCode></BusCode><ERPcheckno></ERPcheckno><CrvouhType></CrvouhType><CrvouhName></CrvouhName>"+
"<CrvouhNo></CrvouhNo><BankType></BankType><FileNames></FileNames><Indexs></Indexs><PaySubNo></PaySubNo>"+
"<RecSubNo></RecSubNo><MCardNo></MCardNo><MCardName></MCardName></rd>";
  string rd_total="";
  int money=0;
  for(int i=1;i<=1000;++i){
  	rd_total+=rd_text.Replace("_X_",i.ToString());
  	money+=i;
  }
  
    string xml_text = "<?xml version=\"1.0\"encoding=\"GBK\"?><CMS><eb><pub><TransCode>PAYENT</TransCode><CIS>46694306-XAAAAA</CIS>"+
"<BankCode>102</BankCode><ID>suzhouTest.y.1102</ID><TranDate></TranDate>NotCare<TranTime>NotCare</TranTime><fSeqno>X</fSeqno>"+
"</pub><in><OnlBatF>1</OnlBatF><SettleMode>0</SettleMode><TotalNum>1000</TotalNum><TotalAmt>"+money.ToString()+"</TotalAmt><SignTime></SignTime>"+
"<ReqReserved1>大批量壓縮測試</ReqReserved1><ReqReserved2></ReqReserved2><zip>"+rd_total+"</zip></in></eb></CMS>";

   string b64_xml=Convert.ToBase64String(Encoding.GetEncoding("GBK").GetBytes(xml_text));
   Console.WriteLine(HttpPostTest("http://127.0.0.1:1398/WebService.asmx/QACCBAL","acct=1102020109000009078"));
   Console.WriteLine();
   Console.WriteLine(HttpPostTest("http://127.0.0.1:1398/WebService.asmx/ICBC_YQHL","ver=0.0.1.0&b64_xml="+b64_xml));
   Console.WriteLine();
   Console.WriteLine(HttpPostTest("http://127.0.0.1:1398/WebService.asmx/QACCBAL","acct=1102020109000009078"));
  }
static string HttpPostTest(string url, string content)
        {
            byte[] bytesToPost = Encoding.GetEncoding("UTF-8").GetBytes(content);
            string cookieheader = string.Empty;
            CookieContainer cookieCon = new CookieContainer();

            #region 建立HttpWebRequest物件
            HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(url);
            #endregion

            #region 初始化HtppWebRequest物件

            httpRequest.CookieContainer = cookieCon;
            httpRequest.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0;)";
            httpRequest.ContentType = "application/x-www-form-urlencoded";
            httpRequest.Method = "POST";
            httpRequest.Timeout = 15 * 1000;

            if (cookieheader.Equals(string.Empty))
            {
                cookieheader = httpRequest.CookieContainer.GetCookieHeader(new Uri(url));
            }
            else
            {
                httpRequest.CookieContainer.SetCookies(new Uri(url), cookieheader);
            }
            #endregion

            string stringResponse = "";
            try
            {

                #region 附加Post給伺服器的資料到HttpWebRequest物件
                httpRequest.ContentLength = bytesToPost.Length;
                System.IO.Stream requestStream = httpRequest.GetRequestStream();
                requestStream.Write(bytesToPost, 0, bytesToPost.Length);
                requestStream.Close();
                #endregion


                #region 讀取伺服器返回資訊


                System.IO.Stream responseStream = httpRequest.GetResponse().GetResponseStream();

                using (System.IO.StreamReader responseReader = new System.IO.StreamReader(responseStream, Encoding.GetEncoding("UTF-8")))
                {
                    stringResponse = responseReader.ReadToEnd();
                }
                responseStream.Close();
                #endregion
            }
            catch (Exception)
            {
                ;
            }
            return  stringResponse;
        }
 }

執行截圖:

大批量支付,一般需要10分鐘左右來處理,因此馬上查詢餘額沒有變化。

對應查詢指令,只要把支付時候的請求編號放到查詢報文的對應欄位QryfSeqno即可。

查詢指令狀態程式碼:BASE64解碼,直接輸出明文:

using System;
using System.Text;
using System.Net;
using System.IO;

class Program
 {
  static void Main(string[] args)
  {

    string xml_text = "<?xml version=\"1.0\" encoding = \"GBK\"?><CMS><eb><pub><TransCode>QPAYENT</TransCode>"+
"<CIS>46694306-XAAAAA</CIS><BankCode>102</BankCode><ID>suzhouTest.y.1102</ID><TranDate></TranDate><TranTime></TranTime><fSeqno></fSeqno></pub>"+
"<in><QryfSeqno>[email protected]</QryfSeqno><QrySerialNo></QrySerialNo></in></eb></CMS>";

   string b64_xml=Convert.ToBase64String(Encoding.GetEncoding("GBK").GetBytes(xml_text));   
   string result=HttpPostTest("http://127.0.0.1:1398/WebService.asmx/ICBC_YQHL","ver=0.0.1.0&b64_xml="+b64_xml);
   int p1=result.IndexOf("ReqResult=");
   int p2=result.LastIndexOf("</string>");
   string result2= Encoding.GetEncoding("GBK").GetString(Convert.FromBase64String(result.Substring(p1+10,p2-p1-10)));
   Console.WriteLine(result2);
   
  }
static string HttpPostTest(string url, string content)
        {
            byte[] bytesToPost = Encoding.GetEncoding("UTF-8").GetBytes(content);
            string cookieheader = string.Empty;
            CookieContainer cookieCon = new CookieContainer();

            #region 建立HttpWebRequest物件
            HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(url);
            #endregion

            #region 初始化HtppWebRequest物件

            httpRequest.CookieContainer = cookieCon;
            httpRequest.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0;)";
            httpRequest.ContentType = "application/x-www-form-urlencoded";
            httpRequest.Method = "POST";
            httpRequest.Timeout = 15 * 1000;

            if (cookieheader.Equals(string.Empty))
            {
                cookieheader = httpRequest.CookieContainer.GetCookieHeader(new Uri(url));
            }
            else
            {
                httpRequest.CookieContainer.SetCookies(new Uri(url), cookieheader);
            }
            #endregion

            string stringResponse = "";
            try
            {

                #region 附加Post給伺服器的資料到HttpWebRequest物件
                httpRequest.ContentLength = bytesToPost.Length;
                System.IO.Stream requestStream = httpRequest.GetRequestStream();
                requestStream.Write(bytesToPost, 0, bytesToPost.Length);
                requestStream.Close();
                #endregion


                #region 讀取伺服器返回資訊


                System.IO.Stream responseStream = httpRequest.GetResponse().GetResponseStream();

                using (System.IO.StreamReader responseReader = new System.IO.StreamReader(responseStream, Encoding.GetEncoding("UTF-8")))
                {
                    stringResponse = responseReader.ReadToEnd();
                }
                responseStream.Close();
                #endregion
            }
            catch (Exception)
            {
                ;
            }
            return  stringResponse;
        }
 }

執行:

可用輸出重定向的方式儲存到檔案檢視,比如:QPAYENT > show.txt

對於回單,經過自己的技術積累,目前提供PDF回單服務,回單格式是自己設計的,比官方的似乎要多一些內容。使用的是破解版商業控制元件,本人在PDF文件中做了說明,僅用於學習和研究之用。另外,PDF回單檔案中文字、圖片等都是可以自己修改,所以回單的意義在於存檔,生產環境資料得到的回單可以去官方網站驗證。如果涉及發工資法律糾紛,需要向銀行索要紙質有蓋章的憑據。

講一下PDF回單的獲取方法:

輸入引數:賬號、記賬日期(年月日yyyyMMdd格式)、時間戳,金額(整數,無小數點)。

輸出:0|錯誤訊息;1|經過BASE64編碼的二進位制檔案流

輸入引數可以通過查詢明細的方式獲取,建議使用0.0.1.0版本的QHISD介面。

如果回單檔案是第一次製作,需要一個過程來校驗獲取資料製作。如果已經存在了,則直接返回該檔案內容。

經測試,單個pdf回單檔案在70K左右,經過編碼後傳輸量大約100K。在Windows10的系統中,測試傳輸單個6M的二進位制檔案通過。

我用.NET程式碼演示一下:

using System;
using System.Text;
using System.Net;
using System.IO;

class Program
 {
  static void Main(string[] args)
  {
   Console.WriteLine(DateTime.Now.ToString());
   string result=HttpPostTest("http://127.0.0.1:1398/WebService.asmx/PDF_Receipt","acct=1102020109000009078&date=20181003&time=2018-10-04-15.00.23.937835&amount=582");
   int p1=result.IndexOf("|");
   int p2=result.IndexOf("</string>");
   if(p1>0 && p2>0 && result[p1-1]=='1'){
   	B64String2File(result.Substring(p1+1,p2-p1-1),"csc_code.pdf");
   	Console.WriteLine("csc_code.pdf saved!");
   }else Console.WriteLine(result);
   Console.WriteLine(DateTime.Now.ToString());
   
  }
static void B64String2File(string B64Str,string fileName)
        {
            byte[] bb= Convert.FromBase64String(B64Str);
            FileStream fs = new FileStream(fileName, FileMode.Create);
            fs.Write(bb, 0, bb.Length);
            fs.Close();
        }
static string HttpPostTest(string url, string content)
        {
            byte[] bytesToPost = Encoding.GetEncoding("UTF-8").GetBytes(content);
            string cookieheader = string.Empty;
            CookieContainer cookieCon = new CookieContainer();

            #region 建立HttpWebRequest物件
            HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(url);
            #endregion

            #region 初始化HtppWebRequest物件

            httpRequest.CookieContainer = cookieCon;
            httpRequest.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0;)";
            httpRequest.ContentType = "application/x-www-form-urlencoded";
            httpRequest.Method = "POST";
            httpRequest.Timeout = 30 * 1000;

            if (cookieheader.Equals(string.Empty))
            {
                cookieheader = httpRequest.CookieContainer.GetCookieHeader(new Uri(url));
            }
            else
            {
                httpRequest.CookieContainer.SetCookies(new Uri(url), cookieheader);
            }
            #endregion

            string stringResponse = "";
            try
            {

                #region 附加Post給伺服器的資料到HttpWebRequest物件
                httpRequest.ContentLength = bytesToPost.Length;
                System.IO.Stream requestStream = httpRequest.GetRequestStream();
                requestStream.Write(bytesToPost, 0, bytesToPost.Length);
                requestStream.Close();
                #endregion


                #region 讀取伺服器返回資訊


                System.IO.Stream responseStream = httpRequest.GetResponse().GetResponseStream();

                using (System.IO.StreamReader responseReader = new System.IO.StreamReader(responseStream, Encoding.GetEncoding("UTF-8")))
                {
                    stringResponse = responseReader.ReadToEnd();
                }
                responseStream.Close();
                #endregion
            }
            catch (Exception)
            {
                ;
            }
            return  stringResponse;
        }
 }

執行截圖:

開啟看看pdf文件:

PHP程式碼用於獲取PDF回單:

<?php
header('charset: GBK');
function send_post($url, $post_data) {
    $postdata = http_build_query($post_data);
    $options = array(
        'http' => array(
            'method' => 'POST',
            'header' => 'Content-type:application/x-www-form-urlencoded',
            'content' => $postdata,
            'timeout' => 30 // 超時時間(單位:s)
        )
    );
    $context = stream_context_create($options);
    $result = file_get_contents($url, false, $context);
    return $result;
}
$post_data = array(
	'acct'=>'1102020109000043166',
	'date'=>'20181003',
	'time'=>'2018-10-04-15.07.05.677042',
	'amount'=>'166'
);
$bank_ret=send_post('http://127.0.0.1:1398/WebService.asmx/PDF_Receipt', $post_data);
$p1= strpos($bank_ret,'|');
$p2= strpos($bank_ret,'</string>');
$b64ed=substr($bank_ret,$p1+1,$p2-$p1-1); 
$content = base64_decode($b64ed); // 解碼
$saveFile = 'php_test.pdf';
file_put_contents($saveFile, $content, true);

?>

回單截圖:

其他程式語言用於獲取PDF回單的程式碼,讀者可自行嘗試。

後記&感悟:

銀企互聯業務絕大部分客戶都是使用NC模式。非NC模式需要使用官方的jar控制元件包來實現NetSafeClient的兩個服務功能,這增加了開發工作量,也限制了開發語言,好處是能得到銀行主動通知。我對非NC模式沒有研究。

在十年的工作中,本人多次協助、指導客戶開發,感覺客戶自身在銀企互聯對接方面做的都不夠完善和健壯。可能職業程式設計師都是趕任務,完成目標即可,基本沒時間也沒動力去回顧。本人技術水平一般,甚至可以說是落後的(畢竟工作崗位不是軟體開發),但有比較充裕的時間專攻一面,挖空心思優化、提升自己的作品。這個開發者版,算是窮盡之力了。

如果自己沒有開發網銀轉換工具:完美轉換.NET,那就不會接觸到那個電子表格商業控制元件。這個控制元件能脫離office環境快速讀寫excel檔案,對於製作pdf檔案大有幫助,說實話很想買一個版權。

如果自己沒有參加2016年年底總行電子銀行召開的銀企互聯研討會,我就沒有想法開發銀企互聯封裝模組,也不會有大量的諸如下載電子回單資訊的應用。封裝模組能大量減輕重複性開發量,當然僅限於我自己的.NET程式,因為別人要理解、使用我的封裝模組也是需要一定時間和精力的。這個模組是核心。

如果自己沒遇到那個使用sap的客戶,那就不會產生開發WebService中介軟體的想法。這個中介軟體是從.NET擴充套件到多語言的通道。

如果上述任意缺少一個,那麼我也無緣製作包含PDF電子回單的開發者版本——實乃一大幸事。

做好事情在於自己,得到認可在於他人。工作中能有創新和突破,是件美妙的事情,至少精神上如此。

在這十年銀企互聯業務、技術支援中,接受過很多人幫助,也幫助過很多人。助人者助己,皆為有緣人。

十年,一劍,於予足矣。

自願:掃碼致謝/捐助商業控制元件。

Bug Report & Advise:[email protected]

下載地址:

https://pan.baidu.com/s/1lrR9qyJcDQdZTO0XtB3lyA