之前写过兴业银行银企直联应当如何查询手续费及退票,但事实上兴业银行还会产生冲账问题。所谓冲账,就是指支付信息错误,导致根本无法到达实际收款银行,这时候因为根本无法到达收款银行,所以也就不会有收款银行回馈的支付失败信息(也就是退票),这时候银行就会自己产生一笔负向交易流水,以便将这笔付款进行冲正。
虽然看起来与退票类似,但两者其实完全不同:
- 退票记录金额是正值,而冲账金额是负值;
- 退票并不会退还手续费,冲账将退还手续费;
- 退票有专门的查询参数,冲账查询方式则与手续费是一致的。
下面先展示一段项目中真实的支付报文,该报文将导致冲账问题,当然关键信息已脱敏
<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
并不是真正的最终状态,出纳等网银操作用户还是可以通过一些操作,在网银中将该笔单据回归到正常可支付状态并进行支付,这样的话,就可能存在系统中支付状态与网银中支付状态不一致,这个风险性还是需要注意的。