首页 > 代码库 > 对接第三方支付接口-类似文件锁的编程小技巧

对接第三方支付接口-类似文件锁的编程小技巧

  在这次对接支付接口的时候,有如下场景:用户还款的时候,APP端只要请求了支付接口后,正常情况下,支付接口会同步返回结果状态,并且异步通知是否成功,支付状态以异步通知为准。这样的场景会出现一个问题,如果APP端请求了支付接口,异步通知迟迟未返回,这样一来,用户还款状态是无法更改(还款的逻辑处理实在异步通知里处理,因为一切以异步通知为准),并且对于用户来说他已经还款了,异步回调没来,可能支付成功,可能支付失败我们不知道,对于用户来说他已经支付还款了,按逻辑这一期还款他无需也不能做其他操作了,所以在回调通知来之前,这一期数据在APP端是需要做一个限制不能让用户操作,在这里加了一个“还款中”状态,标记请求支付接口之后,回调通知来之前的状态(正常情况下这段时间很短,短到让用户无法察觉)。

  加“还款中”的状态是可行的,后台服务端来更新还款记录的状态,由APP端请求,APP端在请求支付接口后支付接口同步返回成功后再请求后台的接口,更新还款记录的状态。这里有个问题:请求支付接口会有一个异步通知返回,我们在异步通知里进行相应的逻辑处理,包括更新还款记录的状态为“已还”,但是APP端同步请求成功后也会请求后台更新这条还款记录为“还款中”,操作同一条数据,我们一开始的做法是更新还款中的时候判断是否是“未还”状态,但是发现如果两个更新操作的方法“同时”处理,即异步通知还未更新为“已还”,更新为“还款中”的方法进去了,检索到这条记录仍是“未还”,同样会处理成“还款中”。这样一来有可能支付成功了,还款记录的状态还是“还款中”的情况,所以我们要解决并发的问题,人为控制如果回调通知已经来了,就没必要在请求后台改成“还款中”。

  这里用到的方法有点类似于“文件锁”,我们通过生成特定文件来标记是否异步通知成功(直接更新为“已还”),如果异步通知成功则不需要更新为“还款中”,如果先改成“还款中”,再改成“已还”也没有问题,如果两个方法同时进行,则根据生成的文件来标记一个方法是否提交完成,这里用到的是Spring的事务控制,另一个事务提交后再执行当前这个方法,主要做法为:在异步回调通知里的方法notifyUrl()开始的时候创建一个文件A,结束时删除这个文件;在更改成“还款中”的方法updateRpmting()开始时候也创建一个文件B,这里要判断A是否存在,如果A存在则当前线程延时60ms,等待notifyUrl()方法执行完毕,这时候还款记录的状态已经改成“已还”,不会再继续执行updateRpmting()方法改成“还款中”,当然在创建文件A的时候也会判断B是否存在,同样处理。这里生成文件的方法需要用java的synchronized来锁住,确保同一时间只有一次调用,生成一个文件。

RechargeUtil.java:(Globals.UNDER_LINE是定义的静态变量表示下划线 “_”)

  //创建一个唯一文件
    public static boolean creatOnlyFile(String rpmtIds,String myType, String otherType){
        // 创建一个files目录下面日期为子目录的rpmtId.txt文件
        String myFileName=files+"/" + DateUtils.date2Str(DateUtils.yyyyMMdd)+"/" + DigestUtils.md5Hex(rpmtIds) + Globals.UNDER_LINE + myType + ".txt";
        String otherFileName=files+"/" + DateUtils.date2Str(DateUtils.yyyyMMdd)+"/" + DigestUtils.md5Hex(rpmtIds) + Globals.UNDER_LINE + otherType + ".txt";
        
        System.out.println("文件地址为:" + myFileName);
        File myFile=new File(myFileName);
        if(!myFile.getParentFile().exists()){
            myFile.getParentFile().mkdirs();
        }
        
        File otherFile=new File(otherFileName);
        
        try {
            return createSynchronizedFile(myFile, otherFile);
        } catch (IOException e) {
            System.out.println("创建文件失败!");
            e.printStackTrace();
        }
        return false;
    }
    
    //同一时间创建一个唯一文件
    public synchronized static boolean createSynchronizedFile(File myFile, File otherFile) throws IOException {
        if(!otherFile.exists()){
            myFile.createNewFile();
            return true;
        }
        return false;
    }

  //删除存在的锁文件
  public static boolean deleteOnlyFile(String rpmtIds,String myType){

      File myFile=new File(files+"/" + DateUtils.date2Str(DateUtils.yyyyMMdd)+"/" + DigestUtils.md5Hex(rpmtIds) + Globals.UNDER_LINE + myType + ".txt");
      if(myFile.exists()){
        return myFile.delete();
      }
      return true;
   }

 

异步回调通知里调用创建文件的方法:

public void notifyUrl(PayRpmtEntity payRpmt, String noAgree ,JSONObject resultMap) throws Exception{
        
        //1.根据“还款-支付记录表”获取rpmtIds,循环修改还款计划状态
        String rpmtIds = payRpmt.getRpmtIds();
        
        //创建一个唯一文件
        int size = 0;
        while (size < 30 && !RechargeUtil.creatOnlyFile(rpmtIds,"01", "02")) {
            Thread.sleep(100);
            size++;
        }
        
        if (size >= 30) {
            log.info("****************************同步回调处理中****************************");
            resultMap.put("ret_code", "1005");
            resultMap.put("ret_msg", "支付处理失败");
            return ;
        }
// TODO 逻辑处理
    // 删除锁文件 RechargeUtil.deleteOnlyFile(rpmtIds, "01"); 
}

 更改成“还款中”的时候:

/**
     * 将还款计划该成还款中
    * @Title: updateRpmt 
    * @param rpmtIdsStr
     * @throws Exception 
     */
    public void updateRpmting(String[] rpmtIdsStr,String rpmtIds) throws Exception{
        
        //创建一个唯一文件
        int size = 0;
        while (size < 30 && !RechargeUtil.creatOnlyFile(rpmtIds,"02", "01")) {
            Thread.sleep(100);
            size++;
        }
        
        if (size >= 30) {
            log.info("****************************状态处理中****************************");
            return ;
        }
        
        // TODO 逻辑处理// 删除锁文件
        RechargeUtil.deleteOnlyFile(rpmtIds, "02");
    }

 

对接第三方支付接口-类似文件锁的编程小技巧