• 基于支付宝微信通知的一种个人收款回调方案


    以下内容仅供学习交流,请勿直接用于商业活动

    ============2019年10月3日更新===============

    经过长时间实践,发现微信和支付宝通知并不是很稳定,有时候会有漏消息的可能,这样对业务会造成很大影响,有些银行的二维码收款可以订阅短信通知,监听短信通知十分稳定,

    经过长达数月的测试,没有一起遗漏,所以值得试试,唯一的缺点是短信接收有点延迟,有时长达1分钟左右

    ==========================================

    最近闲来无事,看到网上有一些免签支付回调的服务商,当时感觉很新奇,于是自己动手看看怎么玩的,先看成果

    App上监听通知并向服务器POST支付信息

    服务端的支付订单表

    下面说原理及流程

    1.App上使用NotificationListenerService监听通知栏通知,一旦微信支付或者支付宝收款收到消息,读取消息的内容,然后使用正则匹配金额

    2.App读取到金额后,构造支付订单,支付订单包含:订单号(App自己生成,不是真实的支付方订单号),金额,App端标识,支付方式,签名(保证数据不被篡改)

    3.App将订单POST到填写的URL中

    4.服务端收到订单信息,先校验签名是否相符,再查看订单是否存在(防止重放攻击),验证通过后存入数据库,并向指定的回调地址发起请求

    5.服务端如果向指定的回调地址发起请求失败,使用定时任务重复发起回调,直到回调成功或达到指定次数

    以上就是全部过程,服务端使用springboot,可以很快速搭建

    当然为了保证可靠性需要给App加固,防止退出,还有这种只能读取到金额,其他信息一无所知,有些局限性

    2019-03-14补充:

    代码很简单,上传github完全是小题大做,下面贴出关键代码

    App部分

    继承NotificationListenerService重写onNotificationPosted方法

      1 //来通知时的调用
      2     @Override
      3     public void onNotificationPosted(StatusBarNotification sbn) {
      4         Notification notification = sbn.getNotification();
      5         if (notification == null) {
      6             return;
      7         }
      8         Bundle extras = notification.extras;
      9         if (extras != null) {
     10             //包名
     11             String pkg = sbn.getPackageName();
     12             // 获取通知标题
     13             String title = extras.getString(Notification.EXTRA_TITLE, "");
     14             // 获取通知内容
     15             String content = extras.getString(Notification.EXTRA_TEXT, "");
     16             Log.i(TAG, String.format("收到通知,包名:%s,标题:%s,内容:%s", pkg, title, content));
     17             //处理
     18             processOnReceive(pkg, title, content);
     19         }
     20     }
     21 
     22 /**
     23      * 消息来时处理
     24      *
     25      * @param pkg
     26      * @param title
     27      * @param content
     28      */
     29     private void processOnReceive(String pkg, String title, String content) {
     30         if (!AppConstants.LISTEN_RUNNING) {
     31             return;
     32         }
     33         if ("com.eg.android.AlipayGphone".equals(pkg)) {
     34             //支付宝
     35             if (checkMsgValid(title,content,"alipay") && StringUtils.isNotBlank(parseMoney(content))) {
     36                 TreeMap<String, String> paramMap = new TreeMap<>();
     37                 paramMap.put("title", title);
     38                 paramMap.put("content", content);
     39                 paramMap.put("identifier", AppConstants.CLIENT_IDENTIFIER);
     40                 paramMap.put("orderid", CommonUtils.randomCharSeq());
     41                 paramMap.put("gateway", "alipay");
     42                 String sign = CommonUtils.calcSign(paramMap, AppConstants.SIGN_KEY);
     43                 if (StringUtils.isBlank(sign)) {
     44                     Log.e(TAG, "签名错误");
     45                     return;
     46                 }
     47                 HttpTask task = new HttpTask();
     48                 task.setOnAsyncResponse(this);
     49                 String json = new Gson().toJson(paramMap);
     50                 task.execute(AppConstants.POST_URL, "sign=" + sign, json);
     51             }
     52         } else if ("com.tencent.mm".equals(pkg)) {
     53             //微信
     54             if (checkMsgValid(title, content, "wxpay") && StringUtils.isNotBlank(parseMoney(content))) {
     55                 TreeMap<String, String> paramMap = new TreeMap<>();
     56                 paramMap.put("title", title);
     57                 paramMap.put("content", content);
     58                 paramMap.put("identifier", AppConstants.CLIENT_IDENTIFIER);
     59                 paramMap.put("orderid", CommonUtils.randomCharSeq());
     60                 paramMap.put("gateway", "wxpay");
     61                 String sign = CommonUtils.calcSign(paramMap, AppConstants.SIGN_KEY);
     62                 if (StringUtils.isBlank(sign)) {
     63                     Log.e(TAG, "签名错误");
     64                     return;
     65                 }
     66                 HttpTask task = new HttpTask();
     67                 task.setOnAsyncResponse(this);
     68                 String json = new Gson().toJson(paramMap);
     69                 task.execute(AppConstants.POST_URL, "sign=" + sign, json);
     70             }
     71         }
     72     }
     73 
     74 /**
     75      * 解析内容字符串,提取金额
     76      *
     77      * @param content
     78      * @return
     79      */
     80     private static String parseMoney(String content) {
     81         Pattern pattern = Pattern.compile("收款(([1-9]\d*)|0)(\.(\d){0,2})?元");
     82         Matcher matcher = pattern.matcher(content);
     83         if (matcher.find()) {
     84             String tmp = matcher.group();
     85             Pattern patternnum = Pattern.compile("(([1-9]\d*)|0)(\.(\d){0,2})?");
     86             Matcher matchernum = patternnum.matcher(tmp);
     87             if (matchernum.find())
     88                 return matchernum.group();
     89         }
     90         return null;
     91     }
     92 
     93     /**
     94      * 验证消息的合法性,防止非官方消息被处理
     95      *
     96      * @param title
     97      * @param content
     98      * @param gateway
     99      * @return
    100      */
    101     private static boolean checkMsgValid(String title, String content, String gateway) {
    102         if ("wxpay".equals(gateway)) {
    103             //微信支付的消息格式
    104             //1条:标题:微信支付,内容:微信支付收款0.01元(朋友到店)
    105             //多条:标题:微信支付,内容:[4条]微信支付: 微信支付收款1.01元(朋友到店)
    106             Pattern pattern = Pattern.compile("^((\[\+?\d+条])?微信支付:|微信支付收款)");
    107             Matcher matcher = pattern.matcher(content);
    108             return "微信支付".equals(title) && matcher.find();
    109         } else if ("alipay".equals(gateway)) {
    110             //支付宝的消息格式,标题:支付宝通知,内容:支付宝成功收款1.00元。
    111             return "支付宝通知".equals(title);
    112         }
    113         return false;
    114     }

    服务端接收代码

    /**
         * 接受App发送的通知内容
         * @param content   通知内容json, {"title": "标题", "content": "内容", "identifier": "app端标识", "orderid": "app生成的唯一订单号", "gateway": "wxpay或alipay"}
         * @param sign      签名,签名方式按照content对应的key1=vaule1&key2=value2...&SECKEY计算md5,key的顺序按字母表的顺序
         * @return
         */
        @RequestMapping(value = "/c/post/notification", method = { RequestMethod.POST })
        @ResponseBody
        public String receiveAppNotification(@RequestBody Map<String, Object> content, String sign) {
            logger.debug("请求参数,content=>{}, sign=>{}", JSON.toJSONString(content), sign);
            if (StringUtils.isBlank(sign) || CollectionUtils.isEmpty(content)) {
                return APIUtil.getReturn(APIConst.PARAM_ERROR);
            }
            //再次验证字段
            String contenttext = (String) content.get("content");
            String identifier = (String) content.get("identifier");
            String orderid = (String) content.get("orderid");
            String gateway = (String) content.get("gateway");
            if (StringUtils.isAnyBlank(contenttext, identifier, orderid, gateway) || !ImmutableList.of("alipay",
                    "wxpay").contains(gateway)) {
                return APIUtil.getReturn(APIConst.PARAM_ERROR);
            }
            //读取金额(单位元)
            Pattern pattern = Pattern.compile("([1-9]\d*\.\d*|0\.\d*[1-9]\d*)");
            Matcher matcher = pattern.matcher(contenttext);
            if (!matcher.find()) {
                return APIUtil.getReturn(APIConst.PARAM_ERROR);
            }
            String amountStr = matcher.group(1);
            logger.debug("解析的金额:{}", amountStr);
            BigDecimal amount = null;
            try {
                amount = new BigDecimal(amountStr);
            } catch (NumberFormatException e) {
                logger.error("金额格式错误: {}", amountStr);
                return APIUtil.getReturn(APIConst.PARAM_ERROR);
            }
    
            //验证签名
            TreeMap<String, Object> paramMap = new TreeMap<>(content);
            Iterator<Map.Entry<String, Object>> it = paramMap.entrySet().iterator();
            StringBuilder sb = new StringBuilder();
            while (it.hasNext()) {
                Map.Entry<String, Object> entry = it.next();
                sb.append(entry.getKey());
                sb.append("=");
                sb.append(entry.getValue());
                sb.append("&");
            }
            sb.append(SIGN_KEY);
            //计算签名
            String calcSign = MD5Util.MD5Encode(sb.toString(), "UTF-8");
            if (!calcSign.equalsIgnoreCase(sign)) {
                return APIUtil.getReturn(1, "签名错误");
            }
    
            //查询订单号是否已经存在
            boolean exist = orderService.checkOrderExist(orderid);
            if (exist) {
                logger.error("订单号:{}已存在", orderid);
                return APIUtil.getReturn(1, "订单号已存在");
            }
    
            //订单写入数据库
            String account = "";
            if (gateway.equals("wxpay")) {
                account = "W" + identifier;
            } else if (gateway.equals("alipay")) {
                account = "A" + identifier;
            }
            MqOrder order = new MqOrder();
            order.setAccount(account);
            order.setAmount(amount);
            order.setGateway(gateway);
            order.setOrderId(orderid);
            order.setStatus(0);
            order.setNotifyCount(0);
            order.setCreateTime(new Date());
            orderService.save(order);
    
            return APIUtil.getReturn(APIConst.OK);
        }

     欢迎学习交流

    加微信时请备注

  • 相关阅读:
    nginx的安装及简单负载均衡配置
    memcached 的配置及 spymemcached 客户端简单使用
    我的github地址
    学习3ds max插件开发过程中的一些小结
    编译opengl编程指南第八版示例代码通过
    lua执行字节码的过程介绍
    lua解析赋值类型代码的过程
    lua解析脚本过程中的关键数据结构介绍
    lua解释执行脚本流程
    lua中的string类型
  • 原文地址:https://www.cnblogs.com/ieinstein/p/10515283.html
Copyright © 2020-2023  润新知