java农业银行-企业银行ERP接口开发(2-接口对接)(汇兑-单笔对公、对私)

本文主要是介绍java农业银行-企业银行ERP接口开发(2-接口对接)(汇兑-单笔对公、对私),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

上一篇文章:java农业银行-企业银行ERP接口开发(1-前期准备)icon-default.png?t=N7T8https://blog.csdn.net/new_public/article/details/133882741

 这篇文章我们主要讲具体的接口对接


我新建了一个工具类,专门用来对接农行接口,并且加上了spring的@Component 注解,把这个类交给spring管理,因为农行直联需要一些配置,比如农行通讯平台ICT的ip地址以及端口,本地开发的时候可以直接写死,但发布上线的时候,这些东西就要放到项目配置文件里面了,总之就是为了一些配置信息方便配置和取值,所以交给spring管理。

建了一个AbcErpToIctSocket工具类,并引入农行通讯平台ICT的IP地址以及端口号,用来对接农行接口


 我以其中一个接口:CFRT02(汇兑-单笔对公,对私)为例


CFRT02接口请求报文格式(这里只展示了此接口特有的字段,请求时,是公共请求字段+接口特有字段)

 CFRT02接口应答报文格式(这里只展示了此接口特有的字段,接口返回时,是公共响应字段+接口特有字段)


根据CFRT02请求报文特有的字段,建一个实体类CFRT02RequestDTO(get set方法太多了,先删掉了,后面自己加),并继承请求基类(请求基类在上篇文章讲了,用于接口请求使用。

package com.sysfunc.express.fin.dto.erp.request;import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;/*** CFRT02(汇兑-单笔对公)请求报文字段* */
@XmlRootElement(name = "ap")
public class CFRT02RequestDTO extends RequestBaseEntity {/*** 金额* */@XmlElement(name = "Amt")private Double amt;/*** ???* */@XmlElement(name = "Cmp")private Cmp cmp;/*** ???* */@XmlElement(name = "Corp")private Corp corp;@XmlAccessorType(XmlAccessType.FIELD)public static class Corp {/*** 预约日期 yyyyMMdd* */@XmlElement(name = "BookingDate")private String bookingDate;/*** 预约时间 HHmmss* */@XmlElement(name = "BookingTime")private String bookingTime;/*** 预约标志* */@XmlElement(name = "BookingFlag")private String bookingFlag;/*** 附言  跨行汇兑时,附言只支持char(60)长* */@XmlElement(name = "Postscript")private String postscript;/*** 他行标志* */@XmlElement(name = "OthBankFlag")private String othBankFlag;/*** 贷方户名* */@XmlElement(name = "CrAccName")private String crAccName;/*** 贷方开户行行名* */@XmlElement(name = "CrBankName")private String crBankName;/*** 贷方行号* */@XmlElement(name = "CrBankNo")private String crBankNo;/*** 借方户名* */@XmlElement(name = "DbAccName")private String dbAccName;/*** 用途* */@XmlElement(name = "WhyUse")private String whyUse;}@XmlAccessorType(XmlAccessType.FIELD)public static class Cmp {/*** 借方省市代码* */@XmlElement(name = "DbProv")private String dbProv;/*** 借方账号* */@XmlElement(name = "DbAccNo")private String dbAccNo;/*** 借方货币号* */@XmlElement(name = "DbCur")private String dbCur;/*** 借方多级账簿* */@XmlElement(name = "DbLogAccNo")private String dbLogAccNo;/*** 贷方账号* */@XmlElement(name = "CrAccNo")private String crAccNo;/*** 贷方省市代码* */@XmlElement(name = "CrProv")private String crProv;/*** 贷方货币号* */@XmlElement(name = "CrCur")private String crCur;/*** 贷方多级账簿* */@XmlElement(name = "CrLogAccNo")private String crLogAccNo;/*** 贷方户名校验标志 1 是 0 否* */@XmlElement(name = "ConFlag")private String conFlag;}
}

根据CFRT02响应报文特有的字段,建一个实体类CFRT02ResponseDTO,并继承响应基类(响应基类在上篇文章讲了,用于接收接口响应报文。 

package com.sysfunc.express.fin.dto.erp.response;import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;/*** CFRT02(汇兑-单笔对公) 应答报文字段* */
@XmlRootElement(name = "ap")
@XmlAccessorType(XmlAccessType.FIELD)
public class CFRT02ResponseDTO extends ResponseBaseEntity {/*** ???* */@XmlElement(name = "Corp")private Corp corp;@XmlAccessorType(XmlAccessType.FIELD)public static class Corp {/*** 落地处理标志 0 不落地 1 落地* */@XmlElement(name = "WaitFlag")private String waitFlag;public String getWaitFlag() {return waitFlag;}public void setWaitFlag(String waitFlag) {this.waitFlag = waitFlag;}}public Corp getCorp() {return corp;}public void setCorp(Corp corp) {this.corp = corp;}
}

接下来在AbcErpToIctSocket工具类建一个方法,用来请求CFRT02接口

    /*** CFRT02 汇兑-单笔对公,对私* */public  Map<String, Object> cfrt02(CFRT02RequestDTO requestEntity) {if (requestEntity == null) {log.error("CFRT02报文请求对象不能为空!");return null;}// 设置 交易代码 为CFRT02,标识本次是请求CFRT02接口requestEntity.setCctransCode(CctransCodeEnum.CFRT02.code());// 请求是否加密,这个参数后面讲requestEntity.setIsEncryption("0");// 请求流水号,本次请求的唯一标识,用于后续查询相关业务,比如 查询交易状态,非常重要if (StringUtils.isBlank(requestEntity.getReqSeqNo())) {requestEntity.setReqSeqNo(UUID.randomUUID().toString().replaceAll("-", ""));}// 开始socket请求Map<String, Object> socketResponseMap = socket(requestEntity, CFRT02ResponseDTO.class);return socketResponseMap;}

上面方法用了一个socket具体请求方法,这个是当前类定义的,所有接口的具体socket请求都调用这个方法。

    /*** 接口socket请求调用* @requestEntity 查询报文* @responseClass 应答报文class* */private  <T extends RequestBaseEntity, R extends ResponseBaseEntity> Map<String, Object> socket(T requestEntity, Class<R> responseClass) {/*** 返回结果:*  requestMessage:请求报文*  responseMessage:应答报文*  responseEntity:应答报文对象*  errorMessage: 错误信息*  isAlreadyResq: 是否已发送请求报文* */Map<String, Object> resultMap = new HashMap<>();// 设置报文基本信息requestBeforeSetCmeBase(requestEntity);// 请求对象转成xml报文String requestMessage = requestObjectToXml(requestEntity, requestEntity.getIsEncryption());if (StringUtils.isBlank(requestMessage)) {resultMap.put("errorMessage", "生成socket请求报文出错");return resultMap;}// 记录请求报文resultMap.put("requestMessage", requestMessage);// 日志唯一标识String uuid = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();log.debug(new StringBuffer(uuid).append(" --> 农行").append(requestEntity.getCctransCode()).append("接口请求,请求报文内容如下:").toString());log.debug(requestMessage);// 开始socket请求SocketClient socketClient = new SocketClient(socketIpAddress, socketPort);Map<String, Object> tcpMap = socketClient.tcp(requestMessage, "GBK");// 记录是否已发送请求报文resultMap.put("isAlreadyResq", tcpMap.get("isAlreadyResq"));// 如果有错误信息if (tcpMap.get("errorMessage") != null) {resultMap.put("errorMessage", tcpMap.get("errorMessage"));return resultMap;}byte[] responseBytes = (byte[]) tcpMap.get("data");log.debug(new StringBuffer(uuid).append(" --> 农行").append(requestEntity.getCctransCode()).append("接口请求,应答报文内容如下:").toString());// 应答报文对象R responseEntity = null;try {// 获得应答报文字符串String responseMessageStr = new String(responseBytes, "GBK");log.debug(responseMessageStr);if (StringUtils.isNotBlank(responseMessageStr)) {resultMap.put("responseMessage", responseMessageStr.replaceAll("\n|\\s", ""));}// bytes转成应答报文对象responseEntity = responseBytesToObject(responseMessageStr, responseClass, uuid);// 记录应答报文对象resultMap.put("responseEntity", responseEntity);} catch (UnsupportedEncodingException e) {log.error("应答报文Bytes 转 字符串失败...");}return resultMap;}

上面代码步骤大致是这样的:

 1 :先设置请求基本信息(requestBeforeSetCmeBase(requestEntity);),就是那些请求公共字段

    /*** 请求前 -> 设置报文基本信息* */private  <T extends RequestBaseEntity> T requestBeforeSetCmeBase(T requestEntity) {requestEntity.setProductID("ICC");requestEntity.setChannelType("ERP");requestEntity.setOpNo("");requestEntity.setCorpNo("");requestEntity.setAuthNo("");if (requestEntity.getReqSeqNo() == null) {requestEntity.setReqSeqNo("");}requestEntity.setReqDate(DateFormatUtil.format("yyyyMMdd", new Date()));requestEntity.setReqTime(DateFormatUtil.format("HHmmss", new Date()));if (requestEntity.getSign() == null) {requestEntity.setSign("");}return null;}

 2 :请求对象转成xml报文

String requestMessage = requestObjectToXml(requestEntity, requestEntity.getIsEncryption());

 农行请求报文有个要求,就是xml报文前面加上几个数字(包头)

意思就是xml报文前面加上7位数字,1位加密位,6位报文字节长度,不足7位后面补0,而报文的长度是按GBK编码进行计算的,一个中文算两个字节长度。

这里说一下,请求报文的各个标签之间不要默认加空格之类的,不要把xml报文拿到Xml格式化工具里面格式化后后直接用。

xml报文应该是这样:0170   <ap><CCTransCode>CFRT02</CCTransCode>...其他内容</ap>

下面代码,XmlUtil.objectToXml 是上一篇文章定义的xml工具类方法。 

    /*** 请求报文对象转成xml字符串,前头加包头* @object 请求对象* @isEncryption 是否加密 0 不加密 1加密* */private  String requestObjectToXml(Object object, String isEncryption) {try {// 对象转成xml字符串String xmlMessage = XmlUtil.objectToXml(object, false, true);// xml报文之前加上包头, 包头第一位为是否为加密包标志,再加上6个字节的字符表示数据包的长度,如果长度不足6位则右边用空格补足String message = new StringBuffer(isEncryption).append(String.format("%-6s", xmlMessage.getBytes("GBK").length)).append(xmlMessage).toString();return message;} catch (JAXBException | UnsupportedEncodingException e) {log.error("请求报文对象转xml报文出错", e);return null;}}

 3 :获取xml请求报文后,创建一个socket请求,SocketClient工具类上篇文章说了

// socketIpAddress,socketPort 农行通讯平台ICT的IP地址以及端口号,前面配置引入了
SocketClient socketClient = new SocketClient(socketIpAddress, socketPort);
Map<String, Object> tcpMap = socketClient.tcp(requestMessage, "GBK");

 4 :将socket请求的返回值解析,获得应答报文等信息

String responseMessageStr = new String(responseBytes, "GBK");

 5 :将应答报文转成应答报文对象

responseEntity = responseBytesToObject(responseMessageStr, responseClass, uuid);
    /*** 应答报文bytes转实体对象* @bytes 报文bytes* @classz 返回对象class* @uuid 日志标记id* */private   <T extends ResponseBaseEntity> T responseBytesToObject(String responseMessageStr, Class<T> classz, String uuid) {if (StringUtils.isNotBlank(responseMessageStr)) {try {// 去掉前面的包头responseMessageStr = responseMessageStr.substring(responseMessageStr.indexOf("<ap>"), responseMessageStr.lastIndexOf("</ap>") + 5);// 报文结果xml转成实体对象return (T) XmlUtil.convertXmlStrToObject(classz, responseMessageStr);} catch (JAXBException e) {log.error(new StringBuffer(uuid).append(" --> 报文xml结果转实体对象失败...").toString(), e);}}return null;}

接口对接到这里就可以,接下来测试一下


先创建CFRT02RequestDTO请求报文对象

CFRT02RequestDTO cfrt02RequestDTO = new CFRT02RequestDTO();
// 本次请求流水号,唯一标识,很重要,后期查询交易状态使用
cfrt02RequestDTO.setReqSeqNo();
// 交易金额
cfrt02RequestDTO.setAmt();
CFRT02RequestDTO.Cmp cmp = new CFRT02RequestDTO.Cmp();
// 借方(扣钱的那一方)省份代码,我定义了一个枚举类,广东省的是44
cmp.setDbProv(ProvinceCodeEnum.Guangdong.code());
// 借方货币代码,人民币是01
cmp.setDbCur(CurrencyEnum.CNY.code());
// 借方账号
cmp.setDbAccNo();
// 贷方(收款的那一方)账号
cmp.setCrAccNo();
// 贷方货币代码
cmp.setCrCur(CurrencyEnum.CNY.code());
// 是否校验贷方户名是否正确
cmp.setConFlag("1");
cfrt02RequestDTO.setCmp(cmp);CFRT02RequestDTO.Corp corp = new CFRT02RequestDTO.Corp();
// 贷方户名
corp.setCrAccName();
// 贷方开户行
corp.setCrBankName();
// 贷方支行号,可以百度查
corp.setCrBankNo();
// 借方户名
corp.setDbAccName();
// 贷方是否农行,0 农行, 1 他行
corp.setOthBankFlag();
// 设置交易附言
if (附言.length() > 30) {// 附言只支持60个字节String feeDesc = 附言.substring(0, 30);corp.setPostscript(feeDesc);
}
cfrt02RequestDTO.setCorp(corp);

注入农行接口请求AbcErpToIctSocket 工具类

@Autowired
private AbcErpToIctSocket abcErpToIctSocket;

开始请求,如果有多笔交易,从客户体验角度来看,最好开启一个或多个线程处理,不然会等很久才请求完。

// 开线程 请求农行接口
new Thread(() -> {// socket 请求Map<String, Object> erpSocketResultMap = abcErpToIctSocket.cfrt02(cfrt02SocketMessageMap.get(payBatchNo));// 处理请求结果this.handlerSocketResponse(erpSocketResultMap);
}).start();

请求完成后,解析返回值,下面代码自己补充对应业务逻辑

    /*** 处理socket应答* @erpSocketResultMap socket返回结果* */private void handlerSocketResponse(Map<String, Object> erpSocketResultMap) {// 记录请求报文if (erpSocketResultMap.get("requestMessage") != null) {erpSocketResultMap.get("requestMessage").toString();}// 记录应答报文if (erpSocketResultMap.get("responseMessage") != null) {erpSocketResultMap.get("responseMessage").toString();}// 如果已经发送socket请求if (erpSocketResultMap.get("isAlreadyResq") != null) {} else {// 如果没有发送请求,那就是直接付款失败}// 响应的返回来源,由农行提供的String respSource = null;// 如果应答对象有值if (erpSocketResultMap.get("responseEntity") != null) {// 公共应答字段ResponseBaseEntity response = (ResponseBaseEntity) erpSocketResultMap.get("responseEntity");// 返回来源respSource = response.getRespSource();// 响应时间response.getRespTime();// 响应描述信息response.getRespInfo();// 响应拓展信息response.getRxtInfo();// 转换成具体的接口响应对象CFRT02ResponseDTO cfrt02Response = (CFRT02ResponseDTO) erpSocketResultMap.get("responseEntity");if (cfrt02Response.getCorp() != null) {// 是否落地处理cfrt02Response.getCorp().getWaitFlag();}} else if (erpSocketResultMap.get("isAlreadyResq") != null) {// 返回对象为空,但发送请求了,可能是因为响应超时,这里要注意,可能交易成功了,所以最好在业务上标记为已成功发送请求,后面在调用其他接口查询交易状态,不可轻易认为是交易失败}// 错误信息if (erpSocketResultMap.get("errorMessage") != null) {erpSocketResultMap.get("errorMessage").toString();}// 根据返回来源判断是否交易失败, -1 表明没有发送请求,交易明确是失败,其他值,就通过查询接口查询确定本次交易状态if (StringUtils.equals("-1", respSource)) {}}

到这里就完事了,下面是我的测试结果


请求报文

应答报文(随机填了些虚假信息,所以失败了)


 其他类


省份枚举类 

/*** 农行企业银行-省区代码* */
public enum ProvinceCodeEnum {Tianjin("02", "天津市"),Shanghai("03", "上海"),Shanxi("04", "山西省"),Neimenggu("05", "内蒙古"),Liaoning("06", "辽宁省"),Jilin("07", "吉林省"),Heilongjiang("08", "黑龙江"),Jiangsu("10", "江苏省"),Beijing("11", "北京市"),Anhui("12", "安徽省"),Fujian("13", "福建省"),Jiangxi("14", "江西省"),Shandong("15", "山东省"),Henan("16", "河南省"),Hubei("17", "湖北省"),Hunan("18", "湖南省"),Zhejiang("19", "浙江省"),Guangxi("20", "广西区"),Hainan("21", "海南省"),Sichuan("22", "四川省"),Guizhou("23", "贵州省"),Yunnan("24", "云南省"),Xizang("25", "西藏区"),Shaanxi("26", "陕西省"),Gansu("27", "甘肃省"),Qinghai("28", "青海省"),Ningxia("29", "宁夏区"),Xinjiang("30", "新疆区"),Chongqing("31", "重庆市"),Dalian("34", "大连市"),Qingdao("38", "青岛市"),Ningbo("39", "宁波市"),Xiamen("40", "厦门市"),ShenZhen("41", "深圳市"),Guangdong("44", "广东省"),Hebei("50", "河北省"),Taiwan("71", "台湾省"),Trade("81", "营业部"),Xianggang("97", "香港"),Aomen("98", "澳门"),headOffice("99", "总行");private String code;private String title;ProvinceCodeEnum(String code, String title) {this.code = code;this.title = title;}public String code() {return this.code;}public String title() {return this.title;}
}

货币枚举类

/*** 农行企业银行-货币代码* */
public enum CurrencyEnum {COMPOSITE("00", "复合币种"),CNY("01", "CNY 人民币"),GBP("12", "GBP 英镑"),HKD("13", "HKD 港币"),USD("14", "USD 美元"),CHF("15", "CHF 瑞士法郎"),SGD("18", "SGD 新加坡元"),SEK("21", "SEK 瑞典克郎"),DKK("22", "DKK 丹麦克郎"),NOK("23", "NOK 挪威克郎"),JPY("27", "JPY 日元"),CAD("28", "CAD 加拿大元"),AUD("29", "AUD 澳大利亚元"),EUR("38", "EUR 欧元"),MOP("81", "MOP 澳门币");private String code;private String title;CurrencyEnum(String code, String title) {this.code = code;this.title = title;}public String code() {return this.code;}public String title() {return this.title;}
}

 码字不易,于你有利,勿忘点赞

千里黄云白日曛,北风吹雁雪纷纷

这篇关于java农业银行-企业银行ERP接口开发(2-接口对接)(汇兑-单笔对公、对私)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/719858

相关文章

Java实现按字节长度截取字符串

《Java实现按字节长度截取字符串》在Java中,由于字符串可能包含多字节字符,直接按字节长度截取可能会导致乱码或截取不准确的问题,下面我们就来看看几种按字节长度截取字符串的方法吧... 目录方法一:使用String的getBytes方法方法二:指定字符编码处理方法三:更精确的字符编码处理使用示例注意事项方

Python+PyQt5开发一个Windows电脑启动项管理神器

《Python+PyQt5开发一个Windows电脑启动项管理神器》:本文主要介绍如何使用PyQt5开发一款颜值与功能并存的Windows启动项管理工具,不仅能查看/删除现有启动项,还能智能添加新... 目录开篇:为什么我们需要启动项管理工具功能全景图核心技术解析1. Windows注册表操作2. 启动文件

Spring三级缓存解决循环依赖的解析过程

《Spring三级缓存解决循环依赖的解析过程》:本文主要介绍Spring三级缓存解决循环依赖的解析过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、循环依赖场景二、三级缓存定义三、解决流程(以ServiceA和ServiceB为例)四、关键机制详解五、设计约

spring IOC的理解之原理和实现过程

《springIOC的理解之原理和实现过程》:本文主要介绍springIOC的理解之原理和实现过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、IoC 核心概念二、核心原理1. 容器架构2. 核心组件3. 工作流程三、关键实现机制1. Bean生命周期2.

解决tomcat启动时报Junit相关错误java.lang.ClassNotFoundException: org.junit.Test问题

《解决tomcat启动时报Junit相关错误java.lang.ClassNotFoundException:org.junit.Test问题》:本文主要介绍解决tomcat启动时报Junit相... 目录tomcat启动时报Junit相关错误Java.lang.ClassNotFoundException

Gradle下如何搭建SpringCloud分布式环境

《Gradle下如何搭建SpringCloud分布式环境》:本文主要介绍Gradle下如何搭建SpringCloud分布式环境问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录Gradle下搭建SpringCloud分布式环境1.idea配置好gradle2.创建一个空的gr

JVM垃圾回收机制之GC解读

《JVM垃圾回收机制之GC解读》:本文主要介绍JVM垃圾回收机制之GC,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、死亡对象的判断算法1.1 引用计数算法1.2 可达性分析算法二、垃圾回收算法2.1 标记-清除算法2.2 复制算法2.3 标记-整理算法2.4

springboot集成Lucene的详细指南

《springboot集成Lucene的详细指南》这篇文章主要为大家详细介绍了springboot集成Lucene的详细指南,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以跟随小编一起... 目录添加依赖创建配置类创建实体类创建索引服务类创建搜索服务类创建控制器类使用示例以下是 Spring

Java调用Python的四种方法小结

《Java调用Python的四种方法小结》在现代开发中,结合不同编程语言的优势往往能达到事半功倍的效果,本文将详细介绍四种在Java中调用Python的方法,并推荐一种最常用且实用的方法,希望对大家有... 目录一、在Java类中直接执行python语句二、在Java中直接调用Python脚本三、使用Run

使用Python开发Markdown兼容公式格式转换工具

《使用Python开发Markdown兼容公式格式转换工具》在技术写作中我们经常遇到公式格式问题,例如MathML无法显示,LaTeX格式错乱等,所以本文我们将使用Python开发Markdown兼容... 目录一、工具背景二、环境配置(Windows 10/11)1. 创建conda环境2. 获取XSLT