基于支付宝微信通知的一种个人收款回调方案(转)
原文地址: https://www.cnblogs.com/ieinstein/p/10515283.html
以下内容仅供学习交流,请勿直接用于商业活动
============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方法
//来通知时的调用 @Override public void onNotificationPosted(StatusBarNotification sbn) { Notification notification = sbn.getNotification(); if (notification == null) { return; } Bundle extras = notification.extras; if (extras != null) { //包名 String pkg = sbn.getPackageName(); // 获取通知标题 String title = extras.getString(Notification.EXTRA_TITLE, ""); // 获取通知内容 String content = extras.getString(Notification.EXTRA_TEXT, ""); Log.i(TAG, String.format("收到通知,包名:%s,标题:%s,内容:%s", pkg, title, content)); //处理 processOnReceive(pkg, title, content); } } /** * 消息来时处理 * * @param pkg * @param title * @param content */ private void processOnReceive(String pkg, String title, String content) { if (!AppConstants.LISTEN_RUNNING) { return; } if ("com.eg.android.AlipayGphone".equals(pkg)) { //支付宝 if (checkMsgValid(title,content,"alipay") && StringUtils.isNotBlank(parseMoney(content))) { TreeMap<String, String> paramMap = new TreeMap<>(); paramMap.put("title", title); paramMap.put("content", content); paramMap.put("identifier", AppConstants.CLIENT_IDENTIFIER); paramMap.put("orderid", CommonUtils.randomCharSeq()); paramMap.put("gateway", "alipay"); String sign = CommonUtils.calcSign(paramMap, AppConstants.SIGN_KEY); if (StringUtils.isBlank(sign)) { Log.e(TAG, "签名错误"); return; } HttpTask task = new HttpTask(); task.setOnAsyncResponse(this); String json = new Gson().toJson(paramMap); task.execute(AppConstants.POST_URL, "sign=" + sign, json); } } else if ("com.tencent.mm".equals(pkg)) { //微信 if (checkMsgValid(title, content, "wxpay") && StringUtils.isNotBlank(parseMoney(content))) { TreeMap<String, String> paramMap = new TreeMap<>(); paramMap.put("title", title); paramMap.put("content", content); paramMap.put("identifier", AppConstants.CLIENT_IDENTIFIER); paramMap.put("orderid", CommonUtils.randomCharSeq()); paramMap.put("gateway", "wxpay"); String sign = CommonUtils.calcSign(paramMap, AppConstants.SIGN_KEY); if (StringUtils.isBlank(sign)) { Log.e(TAG, "签名错误"); return; } HttpTask task = new HttpTask(); task.setOnAsyncResponse(this); String json = new Gson().toJson(paramMap); task.execute(AppConstants.POST_URL, "sign=" + sign, json); } } } /** * 解析内容字符串,提取金额 * * @param content * @return */ private static String parseMoney(String content) { Pattern pattern = Pattern.compile("收款(([1-9]\\d*)|0)(\\.(\\d){0,2})?元"); Matcher matcher = pattern.matcher(content); if (matcher.find()) { String tmp = matcher.group(); Pattern patternnum = Pattern.compile("(([1-9]\\d*)|0)(\\.(\\d){0,2})?"); Matcher matchernum = patternnum.matcher(tmp); if (matchernum.find()) return matchernum.group(); } return null; } /** * 验证消息的合法性,防止非官方消息被处理 * * @param title * @param content * @param gateway * @return */ private static boolean checkMsgValid(String title, String content, String gateway) { if ("wxpay".equals(gateway)) { //微信支付的消息格式 //1条:标题:微信支付,内容:微信支付收款0.01元(朋友到店) //多条:标题:微信支付,内容:[4条]微信支付: 微信支付收款1.01元(朋友到店) Pattern pattern = Pattern.compile("^((\\[\\+?\\d+条])?微信支付:|微信支付收款)"); Matcher matcher = pattern.matcher(content); return "微信支付".equals(title) && matcher.find(); } else if ("alipay".equals(gateway)) { //支付宝的消息格式,标题:支付宝通知,内容:支付宝成功收款1.00元。 return "支付宝通知".equals(title); } return false; }
服务端接收代码
/** * 接受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); }
欢迎学习交流,qq:3168598325