公司突然要对接农行的企业银行接口,实现给员工费用报销,充值退款等操作,现记录一下,避免下次开发不记得,我用的是java开发,sockte请求方式对接,全程基于我个人理解,有些地方可能写得不对,多多见谅
开发的接口如下(随便抽一两个说说就好了):
1. 汇兑-单笔对公(CFRT02)交易
4. 本行汇总批量代发(实时)(IBAF04)
农行对接交易大致流程如下图,企业财务系统ERP(可以理解为客户端或者我们的管理系统)发起交易请求至农行企业通讯平台ICT(这个相当于一个中转站,是一个软件,部署在服务器上面的),通讯平台ICT就会发送请求至农行管理服务端ICS上面,最后就是处理交易。这次对接主要就是弄ERP(客户端)到ICT这部分
前期准备
农行企业通讯平台ICT(中转站)如下图,相信要对接开发的朋友肯定会有,打开登录后,会看到一个监听端口,默认是15999,不是必须要这个东西在你电脑上,服务器上面有也行,只要知道对方的ip地址以及端口号就可以了
SocketClient工具类,用来进行socket请求用的,至于什么是socket请求,自己百度了,下面是我写的请求方法
public class SocketClient {
private final Logger log = LoggerFactory.getLogger(getClass());
// ip
private String host;
// 端口
private Integer port;
/**
* TCP Socket
* */
public Map<String, Object> tcp(String message, String charsetName) {
/**
* 返回结果:<key, Value>
* data: 应答数据
* errorMessage:程序错误信息
* isAlreadyResq: 是否已经发送请求
* */
Map<String, Object> resultMap = new HashMap<>();
if (StringUtils.isBlank(this.host)) {
try {
this.host = this.getLocalIp();
} catch (UnknownHostException e) {
log.error("获取本机ip失败", e);
}
if (StringUtils.isBlank(this.host)) {
resultMap.put("errorMessage", "获取服务器IP地址失败");
return resultMap;
}
}
try {
// 与服务端建立连接
Socket socket = new Socket(this.host, this.port);
// 60s超时
socket.setSoTimeout(60000);
// 建立连接后获得输出流,发送数据
socket.getOutputStream().write(message.getBytes(charsetName));
// 通过shutdownOutput 告诉接收端已经发送完数据,客户端后续只能接受数据
socket.shutdownOutput();
resultMap.put("isAlreadyResq", true);
// 获得响应数据流
InputStream socketInputStream = socket.getInputStream();
// 接收服务端返回来的数据
byte[] bytes = null;
//读取服务器返回的消息
if (socketInputStream != null && !socketInputStream.markSupported()) {
bytes = IOUtils.toByteArray(socketInputStream);
resultMap.put("errorMessage", null);
resultMap.put("data", bytes);
} else {
resultMap.put("errorMessage", "农行直联无响应数据");
}
// 关闭输入流
socketInputStream.close();
// 关闭socket
socket.close();
} catch (ConnectException e) {
String error = new StringBuffer("socket连接失败,ip:").append(this.host).append(",端口:").append(this.port).toString();
resultMap.put("errorMessage", "socket连接失败,请检查农行银企通平台是否已开启");
log.error(error);
} catch (SocketTimeoutException e) {
resultMap.put("isAlreadyResq", true);
resultMap.put("errorMessage", "socket请求超时");
log.error("socket请求超时,请求数据为:" + message);
} catch (IOException e) {
resultMap.put("isAlreadyResq", true);
resultMap.put("errorMessage", "socket请求异常");
log.error("socket请求异常", e);
}
return resultMap;
}
/**
* 获取本机Ip
* */
private static String getLocalIp() throws UnknownHostException {
InetAddress address = null; //获取本机IP地址
address = InetAddress.getLocalHost();
String localIP = address.getHostAddress(); //获取本机IP地址字符串
return localIP;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public SocketClient(String host, Integer port) {
this.host = host;
this.port = port;
}
public SocketClient(Integer port) {
this.port = port;
}
public SocketClient() {}
}
XmlUtil工具类,用来把对象转成xml格式字符串,或xml格式字符串转成对象
public class XmlUtil {
/**
* 将String类型的xml转换成对象
*/
public static Object convertXmlStrToObject(Class<?> clazz, String xmlStr) throws JAXBException {
JAXBContext context = JAXBContext.newInstance(clazz);
// 进行将Xml转成对象的核心接口
Unmarshaller unmarshal = context.createUnmarshaller();
StringReader sr = new StringReader(xmlStr);
Object xmlObject = unmarshal.unmarshal(sr);
return xmlObject;
}
/**
* Object 转 xml字符串
* isFormat:是否需要格式化xml内容
* isDelHead:是否删除头部的 < ?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
* */
public static String objectToXml(Object obj, boolean isFormat, boolean isDelHead) throws JAXBException {
JAXBContext context = JAXBContext.newInstance(obj.getClass());
Marshaller marshaller = context.createMarshaller();
if (isFormat) {
// 格式化内容
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
}
StringWriter writer = new StringWriter();
marshaller.marshal(obj, writer);
String xmlResult = writer.toString();
if (isDelHead) {
// 去掉开头的<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
return xmlResult.replaceAll("\\<\\?xml.*\\?>", "");
}
return xmlResult;
}
}
说说农行的接口对接,其实是通过xml报文形式交互,说白点就是,我发一串xml格式的字符串(请求报文)过去,农行响应返回一串xml字符串(应答报文)回来,然后我们解析返回来的xml字符串,大致就是这个意思。
下面是其中一个请求报文,查账户余额的
而每个接口请求的报文,和返回来的报文,有一部分是相同的,有些是不同的,就是有公共字段,这些字段可以在开发文档里面看得到,我放出来一下
接下来说说开发对接思路,因为请求和响应都是一串xml字符串,为了方便开发,我就把他们定义成了一个实体对象,方便在java代码中操作,我分成了请求报文对象和应答报文对象,因为有公共字段,我又定义了两个基类存放报文公共字段,让后面的实体对象继承。
请求报文公共对象
/**
* ERP TO ICT 请求报文公共字段
* */
@XmlRootElement(name = "ap")
@XmlAccessorType(XmlAccessType.FIELD)
public class RequestBaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 交易代码
* */
@XmlElement(name = "CCTransCode")
private String cctransCode;
/**
* 产品标识 固定为ICC
* */
@XmlElement(name = "ProductID")
private String productID;
/**
* 渠道标识 固定为ERP
* */
@XmlElement(name = "ChannelType")
private String channelType;
/**
* 企业技监局代码/客户号
* */
@XmlElement(name = "CorpNo")
private String corpNo;
/**
* 企业操作员编号
* */
@XmlElement(name = "OpNo")
private String opNo;
/**
* 认证码
* */
@XmlElement(name = "AuthNo")
private String authNo;
/**
* 请求渠道流水号
* */
@XmlElement(name = "ReqSeqNo")
private String reqSeqNo;
/**
* 请求日期 yyyyMMdd
* */
@XmlElement(name = "ReqDate")
private String reqDate;
/**
* 请求时间 HHmmss
* */
@XmlElement(name = "ReqTime")
private String reqTime;
/**
* 数字签名
* */
@XmlElement(name = "Sign")
private String sign;
/**
* 查询时是否加密 0 不加密 1 加密 请求接口时设置
* */
@XmlTransient
private String isEncryption;
}
应答报文公共对象
/**
* ICT TO ERP 应答报文公共字段
* */
@XmlAccessorType(XmlAccessType.FIELD)
public class ResponseBaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 交易代码
* */
@XmlElement(name = "CCTransCode")
private String cCTransCode;
/**
* 返回来源
* */
@XmlElement(name = "RespSource")
private String respSource;
/**
* 应答流水号
* */
@XmlElement(name = "RespSeqNo")
private String respSeqNo;
/**
* 返回日期
* */
@XmlElement(name = "RespDate")
private String respDate;
/**
* 返回时间
* */
@XmlElement(name = "RespTime")
private String respTime;
/**
* 返回码
* */
@XmlElement(name = "RespCode")
private String respCode;
/**
* 返回信息
* */
@XmlElement(name = "RespInfo")
private String respInfo;
/**
* 返回扩展信息
* */
@XmlElement(name = "RxtInfo")
private String rxtInfo;
/**
* 文件标识 0-无文件 1-有文件
* */
@XmlElement(name = "FileFlag")
private Integer fileFlag;
/**
* ???
* */
@XmlElement(name = "Cme")
private Cme cme;
/**
* ???
* */
@XmlElement(name = "Cmp")
private Cmp cmp;
@XmlAccessorType(XmlAccessType.FIELD)
public static class Cme {
/**
* 记录数
* */
@XmlElement(name = "RecordNum")
private Integer recordNum;
/**
* 字段名 |分隔
* */
@XmlElement(name = "RespHead")
private String respHead;
/**
* 字段数
* */
@XmlElement(name = "FieldNum")
private Integer fieldNum;
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class Cmp {
/**
* 私有数据区
* */
@XmlElement(name = "RespPrvData")
private String respPrvData;
/**
* 文件名
* */
@XmlElement(name = "BatchFileName")
private String batchFileName;
/**
* 续查标志
* */
@XmlElement(name = "ContFlag")
private String contFlag;
/**
* 流水状态 具体见TransStaEnum
* */
@XmlElement(name = "TransSta")
private String transSta;
}
}
公司没有用Lombok,我把getset方法删掉了,自己加吧。至于字段上面那些注解,就是用来将对象跟xml进行转换用的。
@XmlRootElement(name = "ap") 定义xml根节点为ap
@XmlAccessorType(XmlAccessType.FIELD) 把所有字段都绑定上xml
@XmlElement(name = "CCTransCode") 定义这个字段对应的xml标签名
@XmlTransient 转换时忽略此字段
由于接口有点多,对应实体对象就不一一放出来了,放一个出来就可以了
总之实体对象这方面,我的做法就是一个接口对应两个实体对象(一个请求,一个响应),具体请求接口实体对象 继承 请求实体基类,响应的实体对象 继承 响应基类,下面是我的代码结构
前期准备结束,具体开发看下期文章。
码字不易,于你有利,勿忘点赞
望长城内外,惟余莽莽;大河上下,顿失滔滔