之前寫過興業銀行銀企直聯應當如何查詢手續費及退票,但事實上興業銀行還會產生衝賬問題。所謂衝賬,就是指支付信息錯誤,導致根本無法到達實際收款銀行,這時候因爲根本無法到達收款銀行,所以也就不會有收款銀行回饋的支付失敗信息(也就是退票),這時候銀行就會自己產生一筆負向交易流水,以便將這筆付款進行衝正。
雖然看起來與退票類似,但兩者其實完全不同:
- 退票記錄金額是正值,而衝賬金額是負值;
- 退票並不會退還手續費,衝賬將退還手續費;
- 退票有專門的查詢參數,衝賬查詢方式則與手續費是一致的。
下面先展示一段項目中真實的支付報文,該報文將導致衝賬問題,當然關鍵信息已脫敏
<SECURITIES_MSGSRQV1>
<XFERTRNRQ>
<TRNUID>000145_DCXJXL1904260004_1</TRNUID>
<XFERRQ>
<XFERINFO>
<ACCTFROM>
<ACCTID>XXXXXX</ACCTID>
<NAME>XXXX信息技術股份有限公司</NAME>
<BANKDESC>興業銀行股份有限公司上海淮海支行</BANKDESC>
<CITY>上海</CITY>
</ACCTFROM>
<ACCTTO INTERBANK="N" LOCAL="Y">
<ACCTID>XXXXX</ACCTID>
<NAME>XXX</NAME>
<BANKDESC>烏魯木齊市</BANKDESC>
<CITY>上海黃浦區</CITY>
</ACCTTO>
<TRNAMT>16.00</TRNAMT>
<PURPOSE>FKB_DCXJXL1904260004_000145</PURPOSE>
</XFERINFO>
</XFERRQ>
</XFERTRNRQ>
</SECURITIES_MSGSRQV1>
這段報文哪裏錯了呢?細看一下,收款銀行ACCTTO.BANKDESC
,這裏居然錯誤的填寫了烏魯木齊市
,事實上,這會導致人行無法找到對應的收款銀行,從而也就導致了衝賬。
下面我們再看下3.4.2 查詢轉賬交易狀態
所查詢到的支付結果
<SECURITIES_MSGSRSV1>
<XFERINQTRNRS>
<TRNUID>190429103101328_3.4.2_1652</TRNUID>
<STATUS>
<CODE>0</CODE>
<SEVERITY>INFO</SEVERITY>
</STATUS>
<XFERINQRS>
<XFERLIST MORE="N">
<FROM>900861323722</FROM>
<TO>900861323722</TO>
<XFER>
<SRVRTID>900861323722</SRVRTID>
<XFERINFO>
<ACCTFROM>
<ACCTID>XXXXXX</ACCTID>
<NAME>XXXX信息技術股份有限公司</NAME>
<CITY>上海</CITY>
</ACCTFROM>
<ACCTTO INTERBANK="N" LOCAL="Y">
<ACCTID>XXXXX</ACCTID>
<NAME>XXX</NAME>
<BANKDESC>烏魯木齊市</BANKDESC>
<CITY>上海黃浦區</CITY>
</ACCTTO>
<CHEQUENUM>8758456</CHEQUENUM>
<CURSYM>RMB</CURSYM>
<TRNAMT>16.00</TRNAMT>
<PURPOSE>FKB_DCXJXL1904260004_000145</PURPOSE>
</XFERINFO>
<XFERPRCSTS>
<XFERPRCCODE>PAYOUT</XFERPRCCODE>
<DTXFERPRC>2019-04-26 16:42:10</DTXFERPRC>
<MESSAGE>交易成功</MESSAGE>
</XFERPRCSTS>
</XFER>
</XFERLIST>
</XFERINQRS>
</XFERINQTRNRS>
</SECURITIES_MSGSRSV1>
這個接口不管怎麼查詢,返回的最終結果都是PAYOUT
,下面我們再看下3.6賬戶餘額和交易流水分頁查詢
中查詢到的交易信息
<SECURITIES_MSGSRSV1>
<SCUSTSTMTTRNRS>
<TRNUID>190426224007047_3.6_1_9469</TRNUID>
<STATUS>
<CODE>0</CODE>
<SEVERITY>INFO</SEVERITY>
</STATUS>
<SCUSTSTMTRS>
<CURDEF>RMB</CURDEF>
<ACCTFROM>
<ACCTID>XXXXX</ACCTID>
</ACCTFROM>
<TRANLIST MORE="N">
<DTSTART>2019-04-26</DTSTART>
<DTEND>2019-04-26</DTEND>
<STMTTRN>
<SRVRTID>9999A5OA</SRVRTID>
<TRNTYPE>DEBIT</TRNTYPE>
<TRNCODE>227</TRNCODE>
<DTACCT>2019-04-26T16:42:10</DTACCT>
<TRNAMT>1.00</TRNAMT>
<BALAMT>49974.00</BALAMT>
<CURRENCY>RMB</CURRENCY>
<MEMO></MEMO>
<CORRELATE_ACCTID></CORRELATE_ACCTID>
<CORRELATE_NAME></CORRELATE_NAME>
<CHEQUENUM></CHEQUENUM>
<BILLTYPE></BILLTYPE>
<BILLNUMBER></BILLNUMBER>
<CORRELATE_BANKNAME></CORRELATE_BANKNAME>
<CORRELATE_BANKCODE></CORRELATE_BANKCODE>
<BUSINESSTYPE>銀行扣款</BUSINESSTYPE>
<ATTACHINFO>2019042600305614580000001</ATTACHINFO>
<HXJYLSBH>H00100201904260010541383430000</HXJYLSBH>
<SUMMNAME>收費</SUMMNAME>
<SUMMDESC>收費</SUMMDESC>
<PURPOSE>企業網銀轉賬手續費;</PURPOSE>
<BRANCHNO>21168</BRANCHNO>
<CHANNELCODE>204</CHANNELCODE>
<CASHFLAG>1</CASHFLAG>
<CBBZ>0</CBBZ>
<BCZBZ>0</BCZBZ>
<ROUTECHOICE></ROUTECHOICE>
<BIZREF></BIZREF>
<TEXT1></TEXT1>
<TEXT2></TEXT2>
<TEXT3></TEXT3>
</STMTTRN>
<STMTTRN>
<SRVRTID>9999A5OA</SRVRTID>
<TRNTYPE>DEBIT</TRNTYPE>
<TRNCODE>231</TRNCODE>
<DTACCT>2019-04-26T16:42:10</DTACCT>
<TRNAMT>16.00</TRNAMT>
<BALAMT>49958.00</BALAMT>
<CURRENCY>RMB</CURRENCY>
<MEMO></MEMO>
<CORRELATE_ACCTID>XXXXX</CORRELATE_ACCTID>
<CORRELATE_NAME>XXX</CORRELATE_NAME>
<CHEQUENUM>118758456</CHEQUENUM>
<BILLTYPE></BILLTYPE>
<BILLNUMBER></BILLNUMBER>
<CORRELATE_BANKNAME>烏魯木齊市</CORRELATE_BANKNAME>
<CORRELATE_BANKCODE></CORRELATE_BANKCODE>
<BUSINESSTYPE></BUSINESSTYPE>
<ATTACHINFO>2019042600305614588000001</ATTACHINFO>
<HXJYLSBH>H00100201904260010541383430000</HXJYLSBH>
<SUMMNAME>匯款</SUMMNAME>
<SUMMDESC>網上匯款</SUMMDESC>
<PURPOSE>FKB_DCXJXL1904260004_000145</PURPOSE>
<BRANCHNO>21168</BRANCHNO>
<CHANNELCODE>204</CHANNELCODE>
<CASHFLAG>1</CASHFLAG>
<CBBZ>0</CBBZ>
<BCZBZ>0</BCZBZ>
<ROUTECHOICE>4</ROUTECHOICE>
<BIZREF></BIZREF>
<TEXT1></TEXT1>
<TEXT2></TEXT2>
<TEXT3></TEXT3>
</STMTTRN>
</TRANLIST>
<LEDGERBAL>
<BALAMT>49958.00</BALAMT>
<DTASOF>2019-04-26</DTASOF>
</LEDGERBAL>
<AVAILBAL>
<BALAMT>49958.00</BALAMT>
<DTASOF>2019-04-26</DTASOF>
</AVAILBAL>
</SCUSTSTMTRS>
</SCUSTSTMTTRNRS>
</SECURITIES_MSGSRSV1>
下面就是同樣通過3.6賬戶餘額和交易流水分頁查詢
中查詢到的衝賬信息
<SECURITIES_MSGSRSV1>
<SCUSTSTMTTRNRS>
<TRNUID>190428154005926_3.6_1_9957</TRNUID>
<STATUS>
<CODE>0</CODE>
<SEVERITY>INFO</SEVERITY>
</STATUS>
<SCUSTSTMTRS>
<CURDEF>RMB</CURDEF>
<ACCTFROM>
<ACCTID>216170100100279407</ACCTID>
</ACCTFROM>
<TRANLIST MORE="N">
<DTSTART>2019-04-28</DTSTART>
<DTEND>2019-04-28</DTEND>
<STMTTRN>
<SRVRTID>07020004</SRVRTID>
<TRNTYPE>DEBIT</TRNTYPE>
<TRNCODE>144</TRNCODE>
<DTACCT>2019-04-28T10:39:56</DTACCT>
<TRNAMT>-16.00</TRNAMT>
<BALAMT>49974.00</BALAMT>
<CURRENCY>RMB</CURRENCY>
<MEMO></MEMO>
<CORRELATE_ACCTID>XXXXX</CORRELATE_ACCTID>
<CORRELATE_NAME>XXX</CORRELATE_NAME>
<CHEQUENUM>118758456</CHEQUENUM>
<BILLTYPE></BILLTYPE>
<BILLNUMBER></BILLNUMBER>
<CORRELATE_BANKNAME>烏魯木齊市</CORRELATE_BANKNAME>
<CORRELATE_BANKCODE></CORRELATE_BANKCODE>
<BUSINESSTYPE></BUSINESSTYPE>
<ATTACHINFO>2019042800323201412000001</ATTACHINFO>
<HXJYLSBH>H00100201904280011321490470000</HXJYLSBH>
<SUMMNAME>衝賬</SUMMNAME>
<SUMMDESC>隔日衝賬</SUMMDESC>
<PURPOSE>無收款行</PURPOSE>
<BRANCHNO>21168</BRANCHNO>
<CHANNELCODE>101</CHANNELCODE>
<CASHFLAG>1</CASHFLAG>
<CBBZ>3</CBBZ>
<BCZBZ>0</BCZBZ>
<ROUTECHOICE>4</ROUTECHOICE>
<BIZREF></BIZREF>
<TEXT1></TEXT1>
<TEXT2></TEXT2>
<TEXT3></TEXT3>
</STMTTRN>
<STMTTRN>
<SRVRTID>07020004</SRVRTID>
<TRNTYPE>DEBIT</TRNTYPE>
<TRNCODE>144</TRNCODE>
<DTACCT>2019-04-28T10:39:56</DTACCT>
<TRNAMT>-1.00</TRNAMT>
<BALAMT>49975.00</BALAMT>
<CURRENCY>RMB</CURRENCY>
<MEMO></MEMO>
<CORRELATE_ACCTID></CORRELATE_ACCTID>
<CORRELATE_NAME></CORRELATE_NAME>
<CHEQUENUM></CHEQUENUM>
<BILLTYPE></BILLTYPE>
<BILLNUMBER></BILLNUMBER>
<CORRELATE_BANKNAME></CORRELATE_BANKNAME>
<CORRELATE_BANKCODE></CORRELATE_BANKCODE>
<BUSINESSTYPE></BUSINESSTYPE>
<ATTACHINFO>2019042800323201412000002</ATTACHINFO>
<HXJYLSBH>H00100201904280011321490470000</HXJYLSBH>
<SUMMNAME>衝賬</SUMMNAME>
<SUMMDESC>隔日衝賬</SUMMDESC>
<PURPOSE>無收款行</PURPOSE>
<BRANCHNO>21168</BRANCHNO>
<CHANNELCODE>101</CHANNELCODE>
<CASHFLAG>1</CASHFLAG>
<CBBZ>3</CBBZ>
<BCZBZ>0</BCZBZ>
<ROUTECHOICE></ROUTECHOICE>
<BIZREF></BIZREF>
<TEXT1></TEXT1>
<TEXT2></TEXT2>
<TEXT3></TEXT3>
</STMTTRN>
</TRANLIST>
<LEDGERBAL>
<BALAMT>49975.00</BALAMT>
<DTASOF>2019-04-28</DTASOF>
</LEDGERBAL>
<AVAILBAL>
<BALAMT>49975.00</BALAMT>
<DTASOF>2019-04-28</DTASOF>
</AVAILBAL>
</SCUSTSTMTRS>
</SCUSTSTMTTRNRS>
</SECURITIES_MSGSRSV1>
可以看到交易發生日期是2019-04-26
,實際衝賬日期是2019-04-28
,雖然銀行方面沒能指出衝賬和交易報文可以通過哪些字段進行對應,但對比兩份報文,可以發現雖然交易的HXJYLSBH
與衝賬的HXJYLSBH
不同,但兩者的CHEQUENUM
、CORRELATE_ACCTID
、CORRELATE_NAME
這三者卻是完全一致的,TRNAMT
則是完全相反,另外衝賬退回的手續費,其HXJYLSBH
與衝賬記錄的HXJYLSBH
也是完全一致的,所以到此我們也就知道了衝賬應該如何關聯到原始交易記錄
- 通過
CHEQUENUM
進行關聯,如果覺得不夠保險,可以增加CORRELATE_ACCTID
、CORRELATE_NAME
、TRNAMT
三者共同判斷 - 衝賬退回的手續費與正常交易的手續費一樣,還是通過
HXJYLSBH
進行關聯
接下來我們就是需要在CIBTransactionHelper
中增加衝賬相關的查詢代碼,代碼涉及的SDK在此處下載,完整的代碼如下,如果需要了解各個接口說明,則可以查看此篇內容
/// <summary>
/// 興業銀行交易輔助類
/// </summary>
public class CIBTransactionHelper
{
private long _cid;
private string _userId;
private string _pwd;
private ICIBTransactionPurposeBuilder _buider;
/// <summary>
/// 轉賬對應的SUMMNAME
/// </summary>
public static string TransactionSummaryName = "匯款";
/// <summary>
/// 退票對應的SUMMNAME
/// </summary>
public static string RefundSummaryName = "匯出退回";
/// <summary>
/// 手續費對應的SUMMNAME
/// </summary>
public static string ChargesSummaryName = "收費";
/// <summary>
/// 衝賬對應的SUMMNAME
/// </summary>
public static string RubricSummaryName = "衝賬";
/// <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>
/// 生成一個用於查詢的TRNUID,注意轉賬之類的業務切記不要採用此方法獲取TRNUID
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public static string GetQueryTRNUID(string key)
{
var tmp = (Math.Abs(Guid.NewGuid().GetHashCode()) % 10000).ToString("0000");
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="refundDayDiff">若需自動查詢交易流水時,查詢幾天內的交易流水,默認按興業銀行文檔設置爲2天</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, int refundDayDiff = 2, IList<STMTTRN> transList = null)
{
var dic = new Dictionary<string, Tuple<STMTTRN, STMTTRN>>();
if (refundList != null && refundList.Count > 0)
{
refundList = refundList.Where(x => x.SUMMNAME == RefundSummaryName).OrderBy(x => x.DTACCT).ToList();
if (refundList.Count > 0)
{
if (transList == null || transList.Count == 0)
{//如果未傳遞交易流水,則自動按退票日期獲取對應日期的所有交易流水
transList = this.GetTransactionRecords(acctid, refundList, refundDayDiff);
}
var query = from refund in refundList
join trans in transList
on refund.MEMO equals trans.HXJYLSBH
where trans.SUMMNAME == TransactionSummaryName
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> list, int dayDiff)
{
var transList = new List<STMTTRN>();
var timeList = list.Select(x => x.DTACCT.Date).Distinct().OrderBy(d => d).ToList();
//雖然底層查詢時是拆分成按每日查詢,但因爲退票或衝賬需要倒查N天的交易流水,所以將日期按連續性拆分成日期範圍還是有必要的
var timeRange = this.GetTimeRange(timeList, dayDiff);
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, int dayDiff)
{
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(dayDiff))
{
//退票需要查詢交易流水日期範圍爲交易當天或交易前一天
//所以如果出現跳日,比如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.SUMMNAME == TransactionSummaryName && this._buider.IsCorrectPurpose(x.PURPOSE))
|| x.SUMMNAME == ChargesSummaryName).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.SUMMNAME == TransactionSummaryName);
if (trans == null)
{
continue;
}
var id = this._buider.GetIdFromPurpose(trans.PURPOSE);
if (string.IsNullOrWhiteSpace(id))
{
continue;
}
//可能會無需手續費
var charge = g.FirstOrDefault(x => x.SUMMNAME == ChargesSummaryName)?.TRNAMT ?? 0;
dic.Add(id, Tuple.Create(trans, charge));
}
}
}
return dic;
}
/// <summary>
/// 根據交易流水獲取其內包含的衝賬記錄及衝賬退回的手續費
/// </summary>
/// <param name="list">交易流水</param>
/// <returns>Tuple.Item1爲衝賬交易流水,需按其值<see cref="STMTTRN.CHEQUENUM"/>憑證代號與歷史交易記錄CHEQUENUM的進行比較,Tuple.Item2爲衝賬時退回的手續費</returns>
public IList<Tuple<STMTTRN, decimal>> GetRubricRecords(IList<STMTTRN> list)
{
var retList = new List<Tuple<STMTTRN, decimal>>();
if (list != null && list.Count > 0)
{
list = list.Where(x => x.SUMMNAME == RubricSummaryName).ToList();
if (list.Count > 0)
{
var groups = list.GroupBy(x => x.HXJYLSBH);//衝賬邏輯與退票本質無差別
foreach (var g in groups)
{
var trans = g.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.CHEQUENUM));//憑證代號不爲空代表衝賬交易記錄,爲空代表衝賬手續費
if (trans == null)
{
continue;
}
var charge = g.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.CHEQUENUM))?.TRNAMT ?? 0;//衝賬退回的手續費
retList.Add(Tuple.Create(trans, charge));
}
}
}
return retList;
}
/// <summary>
/// 根據衝賬記錄獲取其對應的交易記錄
/// </summary>
/// <param name="rubricList">衝賬流水及衝賬手續費</param>
/// <param name="acctid">當前衝賬屬於哪個賬號</param>
/// <param name="rubricDayDiff">若需自動查詢交易流水時,查詢幾天內的交易流水,默認設置爲3天</param>
/// <param name="transList">交易流水,默認爲null,代表按退票流水自動查詢,如果不爲null則與退票流水進行對比</param>
/// <returns>Key爲交易流水id,Tuple.Item1爲交易流水,Tuple.Item2爲衝賬流水,Tuple.Item3爲衝賬手續費</returns>
public IDictionary<string, Tuple<STMTTRN, STMTTRN, decimal>> GetRubricMapping(IList<Tuple<STMTTRN, decimal>> rubricList, string acctid, int rubricDayDiff = 3, IList<STMTTRN> transList = null)
{
var dic = new Dictionary<string, Tuple<STMTTRN, STMTTRN, decimal>>();
if (rubricList != null && rubricList.Count > 0)
{
if (transList == null || transList.Count == 0)
{//如果未傳遞交易流水,則自動按衝賬日期獲取對應日期的所有交易流水
transList = this.GetTransactionRecords(acctid, rubricList.Select(x => x.Item1).ToList(), rubricDayDiff);
}
var query = from rubric in rubricList
join trans in transList
on rubric.Item1.CHEQUENUM equals trans.CHEQUENUM
where trans.SUMMNAME == TransactionSummaryName
select Tuple.Create(trans, rubric.Item1, rubric.Item2);
dic = query.ToDictionary(k => this._buider.GetIdFromPurpose(k.Item1.PURPOSE), v => v);
}
return dic;
}
}
/// <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);
}
/// <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;
}
}
調用示例如下
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 rubricList = helper.GetRubricRecords(transList);
#if DEBUG
if (rubricList.Count == 0)
{
var tmpTransList = new List<STMTTRN>();
//爲方便測試,手工增加一條衝賬記錄及衝賬流水
tmpTransList.Add(new STMTTRN
{
DTACCT = new DateTime(2019, 3, 19),
HXJYLSBH = "H00100201904280011321490470000",//假編號
CHEQUENUM = "110340545",
SUMMNAME = CIBTransactionHelper.RubricSummaryName,
SUMMDESC = "隔日衝賬",
PURPOSE = "無收款行",
TRNAMT=-15.54m,
});
tmpTransList.Add(new STMTTRN
{
DTACCT = new DateTime(2019, 3, 19),
HXJYLSBH = "H00100201904280011321490470000",//假編號
CHEQUENUM = "",
SUMMNAME = CIBTransactionHelper.RubricSummaryName,
SUMMDESC = "隔日衝賬",
PURPOSE = "無收款行",
TRNAMT = -0.60m,
});
rubricList = helper.GetRubricRecords(tmpTransList);
}
#endif
//如果你已經通過GetTransactionRecords獲取並持久化了手續費、HXJYLSBH及CHEQUENUM
//那麼下面Mappding這步就可以忽略,轉爲直接查本地數據庫
var rubricDic = helper.GetRubricMapping(rubricList, mainAccountId, rubricDayDiff: 3, transList: transList);
說完了衝賬,我們再來說下網銀審覈退回經辦,因爲該部分內容不多,就不單獨開篇描述。
正常來講,我們既然通過銀企直聯將支付信息發送給了銀行,那麼我們肯定是要進行支付的,所以一般來說,網銀審批時,基本都是會審批通過,但有時候存在一些特殊情況,雖然支付信息已經同步到了銀行,但財務必需要在網銀中終止該筆交易,也就是財務不進行支付,這時候就會產生退回經辦
操作,具體就是出納和財務複審進行退回經辦,當這兩者操作完後,通過3.4.2 查詢轉賬交易狀態
,我們能查到的狀態爲SEND_BACK
,這時候還需要由操作員進行最終的駁回操作,這樣才能使查詢狀態最終返回值爲CANCEL
,然後問題就出在這個操作員身上,因爲銀企直聯時,需要將U盾插在銀企直聯前置機所在服務器上,也就是說,往往財務並不能直接訪問前置機服務器,那麼這個通過操作員賬號進行駁回的操作也就無法進行,雖然興業銀行有EXPIRED
的處理邏輯,但如果你未設置期望支付時間,那麼你需要花費一個月的時間才能等到EXPIRED
這個狀態,所以這在實際操作中基本是無法被用戶接受的,那這時候,你可以考慮將SEND_BACK
做爲一個終結狀態來進行業務處理。當然因爲實際SEND_BACK
並不是真正的最終狀態,出納等網銀操作用戶還是可以通過一些操作,在網銀中將該筆單據迴歸到正常可支付狀態並進行支付,這樣的話,就可能存在系統中支付狀態與網銀中支付狀態不一致,這個風險性還是需要注意的。