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

java银行支付对账 java对接银行接口_开发语言

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

java银行支付对账 java对接银行接口_xml_02


CFRT02(汇兑-单笔对公,对私)为例


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

java银行支付对账 java对接银行接口_java_03

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

java银行支付对账 java对接银行接口_java_04


根据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报文前面加上几个数字(包头)

java银行支付对账 java对接银行接口_开发语言_05

意思就是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;
    }

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

可以随便在一个类里面定义一个main方法测试,不过需要把AbcErpToIctSocket工具类里面的连接信息写死,就是去掉spring的配置全部写死,前提是本地能直接连ICT,这样就不用老是启动springboot服务,这样工具类就不是注入而是new出来了


先创建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)) {

        }
    }

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


请求报文

java银行支付对账 java对接银行接口_java银行支付对账_06

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

java银行支付对账 java对接银行接口_java银行支付对账_07


 其他类


省份枚举类

/**
 * 农行企业银行-省区代码
 * */
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;
    }
}

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

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