前幾天寫了在興業銀行的銀企直聯中,如何查詢手續費和退票流水,但沒有完整的代碼展示,所以這裏再完整的提供下查詢相關的代碼。封裝代碼不涉及任何外部業務,如果你也正在接入興業銀行,且使用的開發語言是NET,那麼你完全可以發揮拿來主義,完全不需要你修改一行代碼!
首先爲了在轉賬時將企業內部系統業務Id
作爲PURPOSE
,我在這裏定義了一個ICIBTransactionPurposeBuilder
接口,該接口的用途是用於規範Id
與PURPOSE
互轉約束,具體代碼如下:
/// <summary>
/// 興業銀行交易流水用途構建約束接口
/// </summary>
public interface ICIBTransactionPurposeBuilder
{
/// <summary>
/// 根據內部系統業務Id構建Purpose
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
string GetPurpose(string id);
/// <summary>
/// 從交易流水Purpose中獲取內部系統業務Id,注意此處Purpose應當爲網上匯款交易流水中的PURPOSE
/// </summary>
/// <param name="purpose"></param>
/// <returns></returns>
string GetIdFromPurpose(string purpose);
/// <summary>
/// 當前PURPOSE是否符合標準
/// </summary>
/// <param name="purpose"></param>
/// <returns></returns>
bool IsCorrectPurpose(string purpose);
}
同時提供了該接口的默認實現,該實現內部其實什麼都沒做,且其所有方法實現都是virtual
的,所以你完全可以繼承該實現,重寫某些你需要自定義的方法,比如IsCorrectPurpose
。
/// <summary>
/// 興業銀行交易流水用途構建默認實現
/// </summary>
public class CIBTransactionPurposeBuilder : ICIBTransactionPurposeBuilder
{
/// <summary>
/// 從交易流水Purpose中獲取內部系統業務Id,注意此處Purpose應當爲網上匯款交易流水中的PURPOSE
/// </summary>
/// <param name="purpose"></param>
/// <returns></returns>
public virtual string GetIdFromPurpose(string purpose)
{
return purpose;
}
/// <summary>
/// 根據內部系統業務Id構建Purpose
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public virtual string GetPurpose(string id)
{
return id;
}
/// <summary>
/// 當前PURPOSE是否符合標準
/// </summary>
/// <param name="purpose"></param>
/// <returns></returns>
public virtual bool IsCorrectPurpose(string purpose)
{
return true;
}
}
接下來就是具體的CIBTransactionHelper
,在貼上其代碼之前,我先羅列下開發中要注意的問題點,這樣就算你不是NET開發,這篇內容也會對你有一定的幫助:
- 生成一個用於查詢的
TRNUID
,該ID
需要唯一,但又因爲不需要重複使用,所以此處簡單的按年月日+標籤+隨機數
來組織 3.3.6 賬戶餘額和交易流水分頁查詢
時,不允許當日與歷史日期同查,所以此處要做判斷處理,然而又因爲可能業務所在服務與興業銀行銀企直聯服務器時間存在時間差,所以爲一勞永逸的解決問題,在調用接口,直接將DTEND
和DTSTART
設置爲同一天進行查詢(注意有可能一天內的交易流水有多頁記錄,需要按返回的MORE
字段進行判斷是否需要多次查詢),然後外部進行循環以便執行指定日期範圍的查詢- 同一個銀行賬號,有可能在多個業務系統內存在交易記錄,那麼如何判斷查詢的交易流水是否是當前業務系統所屬記錄呢,這時候就需要
ICIBTransactionPurposeBuilder
了,後面會有具體的例子來展示如何處理該問題 - 因爲轉賬存在手續費,所以在進行退票前,業務上完全可以先將交易流水相關的一些核心信息(比如
HXJYLSBH
)以及其對應的手續費先通過Job
等方式預先同步到本地數據庫,那麼在進行退票流水關聯交易流水時,就不用再去通過3.3.6 賬戶餘額和交易流水分頁查詢
查詢前幾天的交易流水,而是轉而進行本地查詢 - 如果你不關心手續費問題,那麼你可能就不會去主動同步交易流水信息,這時候你就要按查詢到的退票記錄,自動推斷其對應轉賬交易可能發生的日期,但因爲實際交易時間可能發生在退票前的N天內,那麼爲了減少查詢次數,需要將所有退票流水推斷到的轉賬時間進行去重,然後再按結果時間範圍進行
3.3.6 賬戶餘額和交易流水分頁查詢
using BEDA.CIB.Contracts;
using BEDA.CIB.Contracts.Requests;
using BEDA.CIB.Contracts.Responses;
/// <summary>
/// 興業銀行交易輔助類
/// </summary>
public class CIBTransactionHelper
{
private long _cid;
private string _userId;
private string _pwd;
private ICIBTransactionPurposeBuilder _buider;
/// <summary>
/// 轉賬對應的BUSINESSTYPE
/// </summary>
public static string TransactionBusinessType = "網上匯款";
/// <summary>
/// 退票對應的BUSINESSTYPE
/// </summary>
public static string RefundBusinessType = "銀行退票";
/// <summary>
/// 手續費對應的BUSINESSTYPE
/// </summary>
public static string ChargesBusinessType = "銀行扣款";
/// <summary>
/// 構造函數
/// </summary>
/// <param name="cid">興業銀行銀企直聯客戶號</param>
/// <param name="userId">興業銀行銀企直聯登錄用戶名</param>
/// <param name="pwd">興業銀行銀企直聯登錄密碼</param>
/// <param name="host">前置機域名,默認爲127.0.0.1</param>
/// <param name="port">前置機端口,默認爲8007</param>
/// <param name="builder">轉賬交易用途構建實現,如果不傳則使用默認實現<see cref="CIBTransactionPurposeBuilder"/></param>
public CIBTransactionHelper(long cid, string userId, string pwd,
string host = "127.0.0.1", int port = 8007, ICIBTransactionPurposeBuilder builder = null)
{
if (cid <= 0 || string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(pwd)
|| string.IsNullOrWhiteSpace(host))
{
throw new ArgumentException();
}
this._cid = cid;
this._userId = userId;
this._pwd = pwd;
this._buider = builder;
if (builder == null)
{
this._buider = new CIBTransactionPurposeBuilder();
}
this.Client = new CIBClient(host, port);
}
/// <summary>
/// 興業銀行銀企直聯客戶端
/// </summary>
public ICIBClient Client { get; set; }
/// <summary>
/// 當退票時查詢幾天內的交易流水,默認按興業銀行文檔設置爲2天
/// </summary>
public int RefundDayDiff { get; set; } = 2;
/// <summary>
/// 生成一個用於查詢的TRNUID,注意轉賬之類的業務切記不要採用此方法獲取TRNUID
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public static string GetQueryTRNUID(string key)
{
var tmp = (Math.Abs(Guid.NewGuid().GetHashCode()) % 1000000).ToString("000000");
return string.Format("{0:yyMMddHHmmssfff}_{1}_{2}", DateTime.Now, key, tmp);
}
/// <summary>
/// 獲取興業銀行3.6查詢接口請求主體
/// </summary>
/// <param name="acctid"></param>
/// <param name="dtStart"></param>
/// <param name="dtEnd"></param>
/// <param name="page"></param>
/// <param name="selType"></param>
/// <returns></returns>
public FOXRQ<V1_SCUSTSTMTTRNRQ, V1_SCUSTSTMTTRNRS> GetCIBRequest_3_6(string acctid, DateTime dtStart, DateTime dtEnd, int page, int selType)
{
return new FOXRQ<V1_SCUSTSTMTTRNRQ, V1_SCUSTSTMTTRNRS>()
{
SIGNONMSGSRQV1 = new SIGNONMSGSRQV1
{
SONRQ = new SONRQ
{
CID = this._cid,
USERID = this._userId,
USERPASS = this._pwd,
}
},
SECURITIES_MSGSRQV1 = new V1_SCUSTSTMTTRNRQ
{
SCUSTSTMTTRNRQ = new SCUSTSTMTTRNRQ
{
TRNUID = GetQueryTRNUID("3.6" + "_" + selType),
SCUSTSTMTRQ = new SCUSTSTMTTRN_SCUSTSTMTRQ
{
VERSION = "2.0",
ACCTFROM = new ACCTFROM
{
ACCTID = acctid
},
INCTRAN = new INCTRAN
{
DTEND = dtEnd,
DTSTART = dtStart,
TRNTYPE = 2,
PAGE = page,
},
SELTYPE = selType
}
}
}
};
}
/// <summary>
/// 獲取退票記錄
/// </summary>
/// <param name="acctid"></param>
/// <param name="dtStart"></param>
/// <param name="dtEnd"></param>
/// <returns></returns>
public IList<STMTTRN> GetRefundRecords(string acctid, DateTime dtStart, DateTime dtEnd)
{
return this.GetRecords(acctid, dtStart, dtEnd, 3);
}
private List<STMTTRN> GetRecords(string acctid, DateTime dtStart, DateTime dtEnd, int selType)
{
var list = new List<STMTTRN>();
dtStart = dtStart.Date;
dtEnd = dtEnd.Date;
if (dtStart <= dtEnd)
{
//歷史與當日不能同查,所以此處要加以判斷,因爲每日流水可能較大,所以此處簡單拆分成按每天查詢
var dt = dtStart;
for (; dt <= dtEnd;)
{
for (var i = 1; ; i++)
{
var rq = GetCIBRequest_3_6(acctid, dt, dt, i, selType);
var rs = this.Client.Execute(rq);
if (rs != null && rs.ResponseSuccess && rs.SIGNONMSGSRSV1?.SONRS?.STATUS?.IsCorrect == true
&& rs.SECURITIES_MSGSRSV1?.SCUSTSTMTTRNRS?.STATUS?.IsCorrect == true
&& rs.SECURITIES_MSGSRSV1.SCUSTSTMTTRNRS.SCUSTSTMTRS?.TRANLIST?.List != null)
{
list.AddRange(rs.SECURITIES_MSGSRSV1.SCUSTSTMTTRNRS.SCUSTSTMTRS.TRANLIST.List);
if (rs.SECURITIES_MSGSRSV1.SCUSTSTMTTRNRS.SCUSTSTMTRS.TRANLIST.MORE == "Y")
{
continue;
}
}
break;
}
dt = dt.AddDays(1);
}
}
return list;
}
/// <summary>
/// 獲取交易記錄(含手續費)
/// </summary>
/// <param name="acctid"></param>
/// <param name="dtStart"></param>
/// <param name="dtEnd"></param>
/// <returns></returns>
public IList<STMTTRN> GetTransactionRecords(string acctid, DateTime dtStart, DateTime dtEnd)
{
return this.GetRecords(acctid, dtStart, dtEnd, 1);
}
/// <summary>
/// 根據退票記錄獲取其對應的交易記錄
/// </summary>
/// <param name="refundList">退票流水</param>
/// <param name="acctid">當前退票屬於哪個賬號</param>
/// <param name="transList">交易流水,默認爲null,代表按退票流水自動查詢,如果不爲null則與退票流水進行對比</param>
/// <returns>Key爲交易流水id,Tuple.Item1爲交易流水,Tuple.Item2爲退票流水</returns>
public IDictionary<string, Tuple<STMTTRN, STMTTRN>> GetRefundMapping(IList<STMTTRN> refundList, string acctid, IList<STMTTRN> transList = null)
{
var dic = new Dictionary<string, Tuple<STMTTRN, STMTTRN>>();
if (refundList != null && refundList.Count > 0)
{
refundList = refundList.Where(x => x.BUSINESSTYPE == RefundBusinessType).OrderBy(x => x.DTACCT).ToList();
if (refundList.Count > 0)
{
if (transList == null || transList.Count == 0)
{//如果未傳遞交易流水,則自動按退票日期獲取對應日期的所有交易流水
transList = this.GetTransactionRecords(acctid, refundList);
}
var query = from refund in refundList
join trans in transList
on refund.MEMO equals trans.HXJYLSBH
where trans.BUSINESSTYPE == TransactionBusinessType
select Tuple.Create(trans, refund);
dic = query.ToDictionary(k => this._buider.GetIdFromPurpose(k.Item1.PURPOSE), v => v);
}
}
return dic;
}
private IList<STMTTRN> GetTransactionRecords(string acctid, IList<STMTTRN> refundList)
{
var transList = new List<STMTTRN>();
var timeList = refundList.Select(x => x.DTACCT.Date).Distinct().OrderBy(d => d).ToList();
//雖然底層查詢時是拆分成按每日查詢,但因爲退票需要倒查兩天的交易流水,所以將日期按連續性拆分成日期範圍還是有必要的
var timeRange = this.GetTimeRange(timeList);
foreach (var t in timeRange)
{
var tmp = this.GetTransactionRecords(acctid, t.Item1.AddDays(-1), t.Item2);
transList.AddRange(tmp);
}
return transList;
}
private IList<Tuple<DateTime, DateTime>> GetTimeRange(IList<DateTime> timeList)
{
var timeRange = new List<Tuple<DateTime, DateTime>>();
var dtStart = timeList[0];
var dtEnd = timeList[0];
for (var i = 1; i <= timeList.Count; i++)
{
DateTime dt = DateTime.MaxValue;
if (i < timeList.Count)
{
dt = timeList[i];
}
if (dt >= dtEnd && dt <= dtEnd.AddDays(this.RefundDayDiff))
{
//退票需要查詢交易流水日期範圍爲交易當天或交易前一天
//所以如果出現跳日,比如03-19和03-21,也應該算是連續日期
dtEnd = dt;
}
else
{
timeRange.Add(Tuple.Create(dtStart, dtEnd));
dtStart = dt;
dtEnd = dt;
}
}
return timeRange;
}
/// <summary>
/// 根據交易流水獲取對應的交易記錄及手續費
/// </summary>
/// <param name="list">交易流水</param>
/// <returns>Key爲交易流水id,Tuple.Item1爲交易流水,Tuple.Item2爲手續費</returns>
public IDictionary<string, Tuple<STMTTRN, decimal>> GetServiceChargesMapping(IList<STMTTRN> list)
{
var dic = new Dictionary<string, Tuple<STMTTRN, decimal>>();
if (list != null && list.Count > 0)
{
//此處判斷PURPOSE是否是當前業務組織的PURPOSE
list = list.Where(x => (x.BUSINESSTYPE == TransactionBusinessType && this._buider.IsCorrectPurpose(x.PURPOSE))
|| x.BUSINESSTYPE == ChargesBusinessType).ToList();
if (list.Count > 0)
{
var groups = list.GroupBy(x => x.HXJYLSBH); // new { x.SRVRTID, x.DTACCT }
#if DEBUG
var tmp = groups.ToList();
#endif
foreach (var g in groups)
{
var trans = g.FirstOrDefault(x => x.BUSINESSTYPE == TransactionBusinessType);
if (trans == null)
{
continue;
}
var id = this._buider.GetIdFromPurpose(trans.PURPOSE);
if (string.IsNullOrWhiteSpace(id))
{
continue;
}
//可能會無需手續費
var charge = g.FirstOrDefault(x => x.BUSINESSTYPE == ChargesBusinessType)?.TRNAMT ?? 0;
dic.Add(id, Tuple.Create(trans, charge));
}
}
}
return dic;
}
}
要用到的BEDA.CIB
可見此處,該SDK支持NET452
及Standard2.0
版本,可以注意到CIBTransactionHelper
構造函數最後一個參數是ICIBTransactionPurposeBuilder
,因爲在興業銀行的測試環境中,同時有其它對接用戶也在進行直聯測試,所以我們此時就需要自定義一個ICIBTransactionPurposeBuilder
,將查詢流水獲取到的數據進行過濾,因爲只是測試,所以在發起3.4.1轉賬匯款指令提交
請求時,傳入的PURPOSE
只是簡單的按特定字符前綴+日期
的格式string.Format("fkb_{0:yyMMddHHmmssfff}", DateTime.Now)
進行組織,所以我們也只需要繼承CIBTransactionPurposeBuilder
並簡單的將IsCorrectPurpose
給重寫下就行,當然實際業務場景中你肯定不能這麼任性。
class CustCIBTransactionPurposeBuilder : CIBTransactionPurposeBuilder
{
public override bool IsCorrectPurpose(string purpose)
{
if (!string.IsNullOrWhiteSpace(purpose))
{
return Regex.IsMatch(purpose, @"^fkb_\d{15}$");
}
return false;
}
}
最後就是調用示例,具體如下
const long cid = 1100343164;
const string uid = "qw1";
const string pwd = "a1111111";//密碼錯誤6次賬號會被永久鎖定無法解鎖
const string ip = "127.0.0.1";
const int port = 8007;
const string mainAccountId = "117010100100000177";
public static void TransactionHelperSample()
{
var helper = new CIBTransactionHelper(cid, uid, pwd, ip, port, new CustCIBTransactionPurposeBuilder());
var transList = helper.GetTransactionRecords(mainAccountId, new DateTime(2019, 3, 19), new DateTime(2019, 3, 19));
var changeDic = helper.GetServiceChargesMapping(transList);
var refundList = helper.GetRefundRecords(mainAccountId, new DateTime(2019, 3, 19), new DateTime(2019, 3, 19));
#if DEBUG
if (refundList.Count == 0)
{
//爲方便測試,手工增加一條退票記錄
//fkb_190319140625534 H00100201903190004631399460000
refundList.Add(new STMTTRN
{
DTACCT = new DateTime(2019, 3, 19),
BUSINESSTYPE = CIBTransactionHelper.RefundBusinessType,
HXJYLSBH = "K00100201903190004631399460000",//假編號
MEMO = "H00100201903190004631399460000",
SUMMNAME = "匯出退回",
SUMMDESC = "匯出退回解付",
PURPOSE = "賬號戶名不符",
});
}
#endif
//如果你已經通過GetTransactionRecords獲取並持久化了手續費及HXJYLSBH
//那麼下面Mappding這步就可以忽略,轉爲直接查本地數據庫
helper.RefundDayDiff = 2;
//人行退票實際允許的範圍是3個工作日,極端情況下會出現7個工作日
//所以此處雖然興業銀行說是隻要查2天範圍,但此處還是允許自定義日期範圍
var refundDic = helper.GetRefundMapping(refundList, mainAccountId, transList);
}
你可以在此處查看完整的示例代碼。