招商银行银企直联开发点滴记录

1. 概述

  • 最近工作中用到了招商银行的银企直联系统,作为资金支出渠道。招行系统提供两种方式与企业财务系统对接:一种是前置机式;一种是嵌入式。而“嵌入式直联方式仅作向下兼容支持,新增客户请使用前置机式直联”。因此使用了前置机方式。
  • 使用到的接口文档为《招商银行银企直连接口说明书version 5.21.0》和《网银互联接口说明书Version 1.0》。针对前者,使用了其3.6节“直接支付”接口进行招行内支付,并使用3.3节“查询支付结果”接口进行结果查询;针对后者,使用其第2章节“网银贷记”接口进行跨行支付,并使用1.2节的“交易查询”接口进行支付结果查询。
  • 前置机的安装参照了文档《银企直联安装流程(测试环境专用)》及《招商银行直联系统开发指南version 3.9.0》,文中讲解得十分详细,并且提供了示例代码。
  • 开发使用语言为JAVA。

2.开发中遇到的问题及解决

  存在许多问题,在使用测试环境账号、参数时无法发现,直到上了生产环境才出现。在此将遇到的一个个问题汇集如下,以期对后来开发人员提供借鉴。

2.1 测试

关于调用查询接口,查询支付结果,使用的时间段的问题:

(1).测试时使用本地的前置机,且一直使用招行商务人员提供的测试账号,发现支付时能够即时返回请求被受理的结果,却查询不到支付记录。经过询问银行技术人员,得知银行测试服务器时间不是当前时间,需要用一个工具软件查询测试服务器日期。使用此日期后查询到了支付结果。

(2).查询交易记录时需要注意,”起始日期和结束日期的间隔不能超过100天”。并且查询时间段应该至少包含支付日期的前一天及后一天,因为在0点附近的交易可能会处于前一天,也可能属于后一天。

  另外,使用前置机,不需要报文格式转换插件,因为采用demo中所示的程序,我们使用的已经是格式三的报文了。

2.2 程序

  使用HTTP方式向前置机发送请求,在处理支付及查询的返回报文时,对示例程序作出少许修改,对多条结果处理的方式如下(以直接支付为例):

    /**
     * 处理返回的报文
     * @param result 前置机返回的报文
     */
    private List<NTQPAYRQZ> processResult(String result) {
        List<NTQPAYRQZ>  ntqpayrqzs = null;
        if (result != null && result.length() > 0) {
            XmlPacket pktRsp = XmlPacket.valueOf(result);
            if (pktRsp != null) {
                String sRetCod = pktRsp.getRETCOD();
                if (sRetCod.equals("0")) {
                        /*start 批量支付封装返回的报文开始*/
                        Vector<Map> vector = pktRsp.getProperty("NTQPAYRQZ");
                        ntqpayrqzs = new ArrayList<NTQPAYRQZ>();
                        if(vector!=null){
                            for (Map map : vector) {
                                NTQPAYRQZ  ntqpayrqz = new NTQPAYRQZ();
                                ntqpayrqz=(NTQPAYRQZ) DataUtil.transMap2Bean(map,ntqpayrqz);
                                ntqpayrqzs.add(ntqpayrqz);
                            }
                        }
                        /*end 批量支付封装返回的报文结束*/

                       /* 此处应为对多比交易返回结果的处理 */

                    //下面为单笔时的处理
                    int sectionSize = pktRsp.getSectionSize("NTQPAYRQZ");
                    Map propPayResult = pktRsp.getProperty("NTQPAYRQZ", 0);//由于本人在项目中是单笔支付的,所以最多会返回一条记录,故此处取0。
                    String sREQSTS = (String) propPayResult.get("REQSTS");
                    String sRTNFLG = (String) propPayResult.get("RTNFLG");
                    if (sREQSTS.equals("FIN") && sRTNFLG.equals("F")) {
                        logger.info("支付失败:"
                                + propPayResult.get("ERRTXT"));
                    } else {
                        logger.info("支付已被银行受理(支付状态:" + sREQSTS + ")");
                    }
                } else if (sRetCod.equals("-9")) {
                    logger.info("支付未知异常,请查询支付结果确认支付状态,错误信息:"
                            + pktRsp.getERRMSG());
                } else {
                    logger.info("支付失败:" + pktRsp.getERRMSG());
                }
            } else {
                logger.info("响应报文解析失败");
            }
        }
        return ntqpayrqzs;
    }
    备注:NTQPAYRQZ 为以返回报文的节点构造的类,以节点NTQPAYRQZ下的各个字段作为类的属性。

其中,用到的工具方法如下:

    /**
     * 取得指定接口的数据记录
     * @param sSectionName
     * @param index 索引,从0开始
     * @return Map
     */
    public Map getProperty(String sSectionName, int index){
        if(data.containsKey(sSectionName)){
            return (Map)((Vector)data.get(sSectionName)).get(index);
        }else{
            return null;
        }
    }  
    /**
     * 取得指定接口的所有数据记录
     * @param sSectionName
     * @return Map
     */
    public Vector getProperty(String sSectionName){
        if(data.containsKey(sSectionName)){
            return (Vector)data.get(sSectionName);
        }else{
            return null;
        }
    }
    // Map --> Bean 1: 利用Introspector,PropertyDescriptor实现 Map --> Bean  
    public static Object transMap2Bean(Map map, Object obj) {  
        try {  
            BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass());  
            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();  
            for (PropertyDescriptor property : propertyDescriptors) {  
                String key = property.getName();  
                if (map.containsKey(key)) {  
                    Object value = map.get(key);  
                    // 得到property对应的setter方法  
                    Method setter = property.getWriteMethod();  
                    setter.invoke(obj, value);  
                }  
            }  
        } catch (Exception e) {  
            logger.error("transMap2Bean Error:",e);  
        }  
        return obj;  
    }  

2.3 生产

(1).生产环境应用服务器连接不上前置机服务器

  运维人员对于连接前置机的服务器IP进行了限制;另外,还需要财务人员远程登录前置机服务器,开启前置机;
  
(2).返回错误信息:有审批业务,不可直接经办

  编写程序时忽略了接口文档中的BUSMOD字段值,采用了文档中的示例BUSMOD= 00001。对比参照之前生产环境前置机的报文,发现支付成功的报文中此字段值为00002,修改程序后成功。此字段表示“支付结算业务所采用的业务审批模式”。
  而报文中的另外一个值:“业务类型”字段BUSCOD,指的是“支付、集团支付或内部转帐等”,如BUSCOD=N02031。这两个字段容易混淆,需特别注意,并且它们的长度也不一样。
  
(3).行内支付使用直接支付接口,返回错误信息:收方必须为商户账户

  咨询招行技术人员得知是因为我们公司在招行的账户受到限制,收款方必须加入白名单才能打款。询问财务人员,确实如此,经加入白名单后,打款成功。

  跨行未受到限制,估计是因为走的是央行的超级网银渠道,所以没有限制。
  
(4).跨行支付收款方行号的问题

  跨行支付时需要输入收款行行号(网银贷记接口中的CDTBRD字段),只能输入总行的行号,输入支行的行号报错。咨询招行技术人员,回复是系统中没有支行的行号。与招行对接时商务人员会提供“人行网银互联(跨行清算系统)联行号信息.txt”,其中有各银行及对应的联行号。
  

2.4 关于并发请求的问题

  在文档《招商银行直联系统开发指南version 3.9.0》的3.4及3.5节,有调用频率及并发性能的说明。
  摘录如下:
  

3.4调用频率
     请求调用间隔:本次请求调用与上次请求调用之间的时间差;
     调用频率控制按接口种类区别控制:
 经办类请求:两次经办类请求最小调用间隔为1秒;
 查询类请求:两次查询类请求最小调用间隔为2秒;
 通知类请求:两次通知类请求最小调用间隔为5秒。

    当调用频率没有满足以上条件时(实际调用间隔小于最小调用间隔),系统默认会内部等待,等待时间为:最小调用间隔 — 实际调用间隔,等待完成后再处理该请求。

    当调用频率满足以上条件时(实际调用间隔大于或等于最小调用间隔),系统立即处理该请求并转发到银行后台系统。

    调用频率控制以登陆用户为单位,不同登陆用户之间不受影响。
3.5 并发性能
    本系统支持多用户并发请求,同一用户的多个请求按照优先队列排队处理,为了避免请求等待时间过长,建议同一用户并发请求不要超过50个,登陆用户数没有限制。

备注:一台电脑只能安装一个招行前置机,多台电脑允许安装多个招行前置机。

  开发、测试、上线此通道,前后经历约一个月,与银行商务及技术人员保持了电话及QQ沟通。感谢银行技术人员耐心的讲解及热情的帮助!

你可能感兴趣的:(支付相关开发)