JavaServer集成支付宝支付、批量付款到支付宝账户简介文档

0.301字数 9587阅读 13643

支付宝简介文档

(适用于ydm-java接口与后台,如有误入,但愿也能给您带来帮助)

此文档写于2017年3月,只能说明此时该文档适用。使用前请查看以下接口支付宝是否提供。

App支付产品

批量付款到支付宝账户

(链接如有发生变化,请在官方文档中寻找此产品,一般情况下,产品名不会发生改变)

1. App支付产品

通俗上讲就是在App中使用支付宝付款,流程上就是:App请求接口(服务端),哪一个用户准备要买什么产品或者是要充多少钱,然后服务端拼接一些必要的参数返回给它,App端通过集成支付宝的SDK,根据接口返回的值去唤醒支付宝进行支付;与此同时,支付宝会异步通知服务端,哪一笔订单支付成功,服务端对充值后的逻辑做判断。(代码目前的做法)

见图

alipay

看完读可知,13,14步骤之前有10,图中的做法是在App端唤醒支付宝支付完成后的回调里请求接口,然后接口去请求支付宝,校验这笔订单完成与否,13,14的步骤为了完成服务端的逻辑。

但实际上,支付宝是这样说的:

支付宝sdk对商户的请求支付数据处理完成后,会将结果同步反馈给商户app端。

同步返回的数据,商户可以按照下文描述的方式在服务端验证,验证通过后,可以认为本次用户付款成功。有些时候会出现商户app在支付宝付款阶段被关闭导致无法正确收到同步结果,此时支付结果可以完全依赖服务端的异步通知。

由于同步通知和异步通知都可以作为支付完成的凭证,且异步通知支付宝一定会确保发送给商户服务端。为了简化集成流程,商户可以将同步结果仅仅作为一个支付结束的通知(忽略执行校验),实际支付是否成功,完全依赖服务端异步通知

2. 批量付款到支付宝账户

即提现;商户(业主)用自己的支付宝转给别人的支付宝账户。

很多应用中涉及到提现,支付宝提现是一种普遍做法(知聊、美丽约等),但是该接口属于支付宝的历史接口,在很长的时间内没有更新,曾经问过客服,在不确定的某天,会将其重写。

采用接口与后台相配合实现提现的功能,App端提交提现申请,后台(服务端)处理数据,跳往支付宝的逻辑,支付宝处理完毕后,同样给后台反馈,只会给成功与失败的反馈,这个地方有很多坑,具体请参见下面的描述。

强烈建议你(您),从官方的Api里寻求了解,获取帮助,再结合鄙人粗糙的代码进行完善或使用

3. App支付详解

1. 前期业主申请操作:官方说明

准入条件

申请前必须拥有经过实名认证的支付宝账户;

企业或个体工商户可申请;(只涉及过企业的)

需提供真实有效的营业执照,且支付宝账户名称需与营业执照主体一致;

网站能正常访问且页面显示完整,网站需要明确经营内容且有完整的商品信息;

网站必须通过ICP备案。如为个体工商户,网站备案主体需要与支付宝账户主体名称一致;

如为个体工商户,则团购不开放,且古玩、珠宝等奢侈品、投资类行业无法申请本产品。

计费模式

费率按单笔计算;

一般行业费率:0.6%;特殊行业费率:1.2%,特殊行业范围包括:手机、通讯设备销售;家用电器;数码产品及配件;休闲游戏;网络游戏点卡、渠道代理;游戏系统商;网游周边服务、交易平台;网游运营商(含网页游戏)。

创建应用

在开放平台创建应用

配置应用

给应用添加App支付功能,这样就可以在你的应用里使用App支付能力。

在使用这些能力的时候,需要在开放平台里进行签约,这时候约定的合同就生效了。也可以代替商户签约。

2. 程序员进行配置

上面提及的操作均应由业主申请完成,推荐他看文档即可,在必要时加以帮助,但一定要让业主自己操作(把前期的锅先由业主背负,微信支付也是同样)

App支付系统架构

alipay流程

安全设计

采用HTTPS协议传输交易数据,防止数据被截获,解密;

采用RSA非对称密钥,明确交易双方的身份,保证交易主体的正确性和唯一性。

言归正传

先登录业主的支付宝开放平台,点击开发者中心-->左侧菜单栏应用,如下图:理想情况下是有一个默认的应用2.0(默认存在,不用管),以及业主创建好的所开发的应用。点进去查看。

支付宝开放平台

配置RSA2密钥或者RSA密钥,2选1即可,17年支付宝新增的RSA2加密方式,但当时前端不能配合这种方式,所以你(您)看到的项目都是RSA方式进行的。公钥放到支付宝平台上,私钥自己保存好,Java采用PCKS8编码的私钥,项目中是放在ALiPayConfig.java下的。

应用配置

官方api文档

接着我们看看AlipayConfig这个类:

前几个获取没什么难度,商户的私钥这里对应上图你的配置,你配置了RSA就用RSA的,配置了RSA2就用RSA2的,支付宝的公钥也是要对应好,包括pay_sign_type这个值也要和你的选择对应好,RSA就填RSA,RSA2就填RSA2;私钥什么的要用pck格式的,不需要加空格,完整的一行复制下来;接着就剩下填写回调地址了,在本地的情况下,你可以参考下方注释提供的外网映射,大体意思就是配置一下,访问一个虚拟的域名能指向自己的Controller,把这个提供给支付宝,就能在本地测试支付了。正式发布的时候,读取域名即可。

理想情况下,只需要更改这里的配置,就可以完成支付的需求,当然如果后期表结构有变化,小幅度改改也是可以用的。

publicclassAlipayConfig{// 合作身份者ID,以2088开头由16位纯数字组成的字符串publicstaticStringpartner ="";//appid    在开放平台的应用里可以得到  一般是YYYYDDMM等publicstaticStringappid ="";//商户支付宝账号publicstaticStringseller_email="1234567@qq.com";//商户真实姓名publicstaticStringaccount_name ="XXXX科技有限公司";// 商户的私钥//RSA2public static String private_key = "一长串放在这里";//RSApublicstaticStringprivate_key ="";//支付宝的公钥  RSA2//public static String ali_public_key = "";//支付宝的公钥  RSApublicstaticStringali_public_key ="";//接口名称publicstaticStringmethod ="alipay.trade.app.pay";//调用的接口版本,固定为:1.0publicstaticStringversion ="1.0";/**

* 支付宝服务器主动通知商户服务器里指定的页面http/https路径。建议商户使用https

* 实际上https很贵。没有用过https.

* 这里需要测试的话,需要用外网测试。https://www.ngrok.cc/  这里有免费和付费的,实际上,免费用一下就可以了。

*/publicstaticStringnotify_url = BaseHandler.PATHS+"/LiaoBanApi/ALiPay/AfterPayNotify";//销售产品码,商家和支付宝签约的产品码,为固定值QUICK_MSECURITY_PAYpublicstaticStringproduct_code ="QUICK_MSECURITY_PAY";//商品的标题/交易标题/订单标题/订单关键字等。publicstaticStringsubject ="XXX-金币充值";//↑↑↑↑↑↑↑↑↑↑请在这里配置您的基本信息↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑// 调试用,创建TXT日志文件夹路径publicstaticStringlog_path ="D:\\";// 字符编码格式 目前支持 gbk 或 utf-8publicstaticStringinput_charset ="utf-8";// 签名方式 (支付回调签名方式)publicstaticStringpay_sign_type ="RSA";// 签名方式 不需修改(退款签名方式)publicstaticStringsign_type ="MD5";  }

如果配置了以上的东西还不能用,那很好,证明你(您)可以重写这一块了233;不知大体有没有说明白,支付宝支付就是APP走一下接口,告诉接口,谁要花多少钱,然后接口根据一堆配置信息(里面有账户的一些信息,私钥),按照支付宝的规矩拼接参数,加签,然后扔给App端,App端拿着它就能调起支付宝客户端了,支付成功后呢,支付宝会告诉咱们给它写的回调地址,在回调的Controller里完成给用户支付成功后的逻辑。在ALiPayService里大多完成业务所需的,然后按照支付宝规矩办事,在业务那一块你(您)看到的应该是对于金币的一些处理,这个还需要根据业务进行部分的梳理。

ALiPayController、ALiPayService详解:

在Controller对参数做了简单的判断,传入的参数为用户id,充值套餐id,用户自定义充值的钱数(用户选择套餐就不传自定义金额,传自定义金额就不传套餐id),这里还是根据自己业务所需进行设计吧,跳入Service层。

/**  * 返回App端调起支付宝的值  * 用户选择自定义充值 token_package_id  不传值  * 用户选择充值套餐    money 不传值  *@parampersion_id    充值用户的id  *@paramtoken_package_id        套餐id  *@parammoney        用户选择自定义充值金额  *@return*/@ResponseBody@RequestMapping("getaliorder")publicObjectgetAliOrder(Integer persion_id,Integer token_package_id,Integer money){//验证参数if(ObjectUtil.isPassInteger(persion_id)){if(ObjectUtil.isPassInteger(token_package_id) || ObjectUtil.isPassInteger(money)){returnaLiPayService.getAliOrder(persion_id, token_package_id, money);          }else{returnnewResultData(404,"客户端传参有误。",null);          }      }else{returnnewResultData(404,"客户端传参有误。",null);      }  }

Service层就显得很冗余了,完成一些自己的业务逻辑,然后根据支付宝的要求拼接参数;

首先在充值表里记录了这次行为的订单号(具有唯一性)以及充了多少钱或者选了哪个套餐,为后期回调的时候做准备;

然后拼接参数,这些参数在官方文档里有说明,把表格里要求必填的参数(在ALiPayConfig文件里)都给支付宝准备好了,阅读文档可知,还需要你按照请求示例来进行拼接,经历九九八十一难后最终扔给App端一个字符串......

请求参数组装分下列3步,以最后第三步获取到的请求为准 :

1 请求参数按照key=value&key=value方式拼接的未签名原始字符串;(展开:肯定不想一个一个的拼,就按照map的方式进行处理;这里业务参数 biz_content这里有个坑->所有的value就算是字符串也要加上双引号,类似于这样{"key":"vaule"},即使value是个字符串,拼好业务参数后,也放入map里)

2 再对原始字符串进行签名,参考签名规则;(展开:实际上支付宝提供了加签的jar包,项目已经集成好,主要是alipay-sdk-java20170209153223.jar这个以及它的源码jar,它本身就需要你传一个map,商户自己的私钥以及编码格式;这里的jar包暂不清楚是否已经支持了RSA2的方式,因为到离开时,只测试了RSA的方式)

3 最后对请求字符串的所有一级value(biz_content作为一个value)进行encode,编码格式按请求串中的charset为准,没传charset按UTF-8处理,获得最终的请求字符串。(展开:将得到的sign也放到map里,新创建一个新的map,用来存放进行encode之后的参数;处理之后专门把sign参数拿出来的原因是因为支付宝给的示例里把sign参数在最后面,当初为了避嫌,就这么做了,现在看起来没什么影响。调用了AliPayCore里的一个方法是为了把map转成key=value&形式的字符串。实际上AliPayCore这个类是支付宝给的旧的示例,为了让它有用,这里用了一下)

4 到这里把字符串扔给前端让调支付宝就好了。(之前那个biz_content(写成了{"key":value}因为那个时候value是String类型的,以为不用加双引号)的坑时,给Android没问题,给iOS就死活不行,而且还是10.0之后的支付宝版本,纠结了几天,还惊动了支付宝那边的技术)

publicObject getAliOrder(Integer persion_id,Integer token_package_id,Integer money){      Recharge recharge =newRecharge();//充值实体类//生成自己的订单号recharge.setOrdersn(DateUtil.getDays() + UUIDHashCode.getOrderIdByUUId());//处理些自己的业务      --------    startrecharge.setRecharge_type(1);//充值类型为支付宝充值recharge.setPersion_id(persion_id);//谁充的钱recharge.setCreate_date(newDate());//用户点击支付的时间DecimalFormat  df =newDecimalFormat("#0.00");//用来格式化金额Stringtotal_fee ="";if(token_package_id ==null&& money !=null){//用户自定义充值金额recharge.setToken_package(0);//用户自定义金额,套餐id为0total_fee =String.valueOf(df.format(money));      }elseif(token_package_id !=null&& money ==null){//用户选择充值套餐//验证套餐TokenPackage tokenPackage = tp_dao.selectByKey(token_package_id);if(tokenPackage !=null){if(tokenPackage.getMoney() >0){                  total_fee =String.valueOf(df.format(tokenPackage.getMoney()));                  recharge.setToken_package(token_package_id);              }          }else{returnnewResultData(404,"请重新选择后重试。","充值套餐不存在。");          }      }else{returnnewResultData(404,"您操作有误!","用户充值时,两者都选择了,或者都没有选择。");      }      recharge.setAmount(Double.parseDouble(total_fee));//放入用户充值的金额//申请支付recharge.setState(1);//支付状态  1为申请支付recharge.setType(1);//这个值默认为1,没有意义,后期设计时可以去掉//保存一条充值记录int count = recharge_dao.insert(recharge);//处理些自己的业务      --------    end/**

* 拼接公共参数,一般情况下无需修改

*/if(count ==1){//生成待签名字符串1HashMap waitSignStr =newHashMap();/**

* 公共参数

*/waitSignStr.put("app_id", ALiPayConfig.appid);//支付宝分配给开发者的应用IDwaitSignStr.put("method", ALiPayConfig.method);//接口名称waitSignStr.put("format","JSON");//仅支持JSONwaitSignStr.put("charset", ALiPayConfig.input_charset);//商户网站使用的编码格式,固定为UTF-8waitSignStr.put("sign_type", ALiPayConfig.pay_sign_type);//签名类型,目前仅支持RSA、RSA2。waitSignStr.put("version", ALiPayConfig.version);//调用的接口版本,固定为:1.0waitSignStr.put("timestamp",newSimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(newDate()));//发送请求的时间waitSignStr.put("notify_url", ALiPayConfig.notify_url);//支付宝服务器主动通知商户网站里指定的页面http路径。/**

* 业务参数

*/Stringbiz_content ="{\"subject\":\""+ALiPayConfig.subject+"\",\"out_trade_no\":\""+recharge.getOrdersn()+"\",\"total_amount\":\""+total_fee+"\",\"product_code\":\"QUICK_MSECURITY_PAY\"}";          waitSignStr.put("biz_content", biz_content);//业务参数Stringsign ="";try{              sign = AlipaySignature.rsaSign(waitSignStr, ALiPayConfig.private_key, ALiPayConfig.input_charset);          }catch(Exception e) {              e.printStackTrace();              log.error("出错了!!!!!!!!!~~~~~~~~~~");          }          waitSignStr.put("sign", sign);          log.info(ALiPayCore.createLinkString(waitSignStr));//新建一个map用于存放将URL进行编码。HashMapnewWaitSign=newHashMap();//循环mapSet sets = waitSignStr.keySet();for(Stringstring :sets) {try{newWaitSign.put(string,URLEncoder.encode(waitSignStr.get(string),"UTF-8"));              }catch(Exception e) {                  log.error("*********************拼接支付宝参数进行url编码出错。**********************");              }          }StringurlSign =newWaitSign.get("sign");newWaitSign.remove("sign");StringnewSign= ALiPayCore.createLinkString(newWaitSign) +"&sign="+ urlSign;          log.info(newSign);          HashMap ordermap =newHashMap();          ordermap.put("orderString",newSign);returnnewResultData(1,"success", ordermap);      }else{returnnewResultData(0,"请重新进入充值界面后再试。","save_error");//保存订单信息异常}  }

接下来就是回调了,回调地址取决于ALiPayConfignotify_url,要测试时一定要配好外网映射或者在服务器上进行,测试金额灵活一些,0.01就可以。支付宝回调说明

值得关注的有两个方面:

一,得到异步通知,处理完自己的逻辑后,必须给支付宝反馈success,不带引号,否则支付宝会认为你没有收到,会按照一定时间策略反复通知你;

二,验签(支付宝的jar提供方法)后,还需要判断一些条件,判断订单号、金额、商户id等是不是自己的,防止别人恶意攻击。

在第二步以后,就可以给订单进行业务的处理了,加金币啊什么的,或者发货什么的;记得给支付宝说success(out.print("success");)

/**

* 处理支付宝给的回调

* @param request

* @param out

*/@TransactionalpublicvoidAliPayAfter(HttpServletRequest request,PrintWriter out){//获取支付宝POST过来反馈信息Map params = GetInfoFromALiPay(request);//1.验证签名booleanoneStep =false;try{          oneStep = AlipaySignature.rsaCheckV1(params, ALiPayConfig.ali_public_key,"UTF-8");      }catch(AlipayApiException e) {          e.printStackTrace();log.error("验签出错了...........");      }//第一步if(oneStep){//验证out_trade_no、total_amount、seller_id 是否是自己的if(ALiPayConfig.partner.equals(params.get("seller_id"))){              Recharge recharge = recharge_dao.selectByOrderSn(params.get("out_trade_no"));if(recharge !=null){intcount = recharge.getAmount().compareTo(Double.parseDouble(params.get("total_amount")));if(count ==0){if(ALiPayConfig.appid.equals(params.get("app_id"))){//支付成功的通知if("TRADE_FINISHED".equals(params.get("trade_status")) ||"TRADE_SUCCESS".equals(params.get("trade_status"))){//设置支付宝订单号recharge.setAlipayordersn(params.get("trade_no"));                              recharge.setState(2);//状态为成功inta = recharge_dao.updateByAlipayordersn(recharge);if(a ==1){/**

* 支付成功,以下内容为走自己的业务了

*///用户的金币增加Persion persion =newPersion();                                  persion.setId(recharge.getPersion_id());if(recharge.getToken_package() !=null&& recharge.getToken_package() ==0){//用户自定义充值金额log.info("**************************用户自定义充值金额**************************");                                      BigDecimal rmb =newBigDecimal(recharge.getAmount());//充值的RMB//金币与RMB兑换比例Config config = configMapper.queryConfigById(1);//换算成金币persion.setToken(newBigDecimal(config.getConfig_value()).multiply(rmb).intValue());                                  }elseif(ObjectUtil.isPassInteger(recharge.getToken_package())){log.info("**************************用户选择充值套餐**************************");                                      TokenPackage tp = tp_dao.selectByKey(recharge.getToken_package());                                      persion.setToken(tp.getToken_count() + tp.getGive());                                  }                                  persion.setVersion(recharge.getPversion());//乐观锁//给用户加钱inttokenAdd = persion_dao.addPersionToken(persion);if(tokenAdd ==1){log.info("支付宝支付回调,增加用户金币成功!");                                      out.println("success");//记得处理完自己的业务后告诉支付宝做完了,必须返回success,不然支付宝会一直请求。}else{                                      tokenAdd =1/0;log.error("支付宝支付回调,增加用户金币失败!");                                  }                              }else{log.error("支付宝回调更新订单状态成功。-----失败。");                              }                          }else{//支付失败recharge.setAlipayordersn(params.get("trade_no"));                              recharge.setState(3);inta =recharge_dao.updateByAlipayordersn(recharge);if(a ==1){log.info("支付宝回调更新订单状态失败。-----成功。");                                  out.println("success");                              }else{log.error("支付宝回调更新订单状态失败。-----失败。");                              }                          }                      }else{log.error("支付宝支付回调验证失败,回调中app_id验证失败。");                      }                  }else{log.error("支付宝支付回调验证失败,回调中total_amount验证失败。");                  }              }else{log.error("支付宝支付回调验证失败,回调中out_trade_no验证失败。");              }          }else{log.error("支付宝支付回调验证失败,回调中seller_id验证失败。");          }      }else{log.error("支付宝支付回调验证失败,验证是否是支付宝发来的通知,失败!");      }  }  @SuppressWarnings("unchecked")publicMap GetInfoFromALiPay(HttpServletRequest request) {      Map params =newHashMap();      Map requestParams = request.getParameterMap();for(Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {Stringname = (String) iter.next();String[] values = (String[]) requestParams.get(name);StringvalueStr ="";for(inti =0; i < values.length; i++) {              valueStr = (i == values.length -1) ? valueStr + values[i] : valueStr + values[i] +",";          }//乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化//valueStr = new String(valueStr.getBytes("ISO-8859-1"), "gbk");params.put(name, valueStr);      }returnparams;  }

4. 批量付款到支付宝账户详解

1. 前期业主申请操作:

找文档

这个藏得很深的,因为在支付宝的明面上看不到它,不像App支付,在类似产品大全的页面能找到;首先进入支付宝的文档中心在左侧找到历史接口,但愿你看到的还是这个样子,如果在那一栏底下也标注了新的接口,那么你就重写它吧(笑)。

历史接口

业主签约

由于属于历史接口,支付宝的考量可能是不建议新商家接入,所以签约的方式比较繁杂,需要App支付产品签约成功后,再找人工客服签约。

人工客服可以通过登录支付宝开放平台,如图1所示的地方唤醒;唤醒的是一只机器人,和它聊三句以上的话,会提醒接入人工,如图2。然后就可以和人工说我需要接入批量付款到支付宝账户这个有密接口,跟着她的节奏来进行了。这一步也是需要业主完成的,因为有业主需要考量的东西,程序员做不了主。

图1

图2

人工客服大概会说,该接口每笔会收取0.5%的手续费,不满1元按1元算,最高不会超过25元,您同意吗?,同意就签约了。一般这个费用是转嫁给用户的,看业主如何考量了。

2.程序员进行配置、抒写:

获取PID、MD5Key

pid就不说了,以2088开头的数字;

配置密钥,参照文档上指引的地方,把MD5那个key保存下来,用于请求数据的签名和支付宝返回数据的验签(后期才注意到,在“提现”这一模块,签名是用MD5签名的)。这里就不放图了,支付宝的网页跳来跳去,而且经常改动,无法确切它的位置,文档里倒是更新开很快,这点比某信要强多了。

简单综述一下:

用户,在App中填写好自己的支付宝真实姓名以及对应的支付宝账户,然后在提现那个地方,输上提现的金额,点击提交,接口就接收了这个数据,做一定的处理(比如转嫁手续费什么的),在提现表里有一个状态标识是提现申请、处理中、成功、失败;后台处理申请的提现,拼接一些参数给支付宝,处理成功后,支付宝给回馈,成功的成功,失败的失败。成功,失败拿系统消息作为提示,失败之后返回给用户金钱,可以重新提交申请。失败的原因可能是用户支付宝填的不正确,正常情况下是这样的。但是有很多意外情况,也就带来了很多坑,下面会拿粗体涉及。

App接口处理方面

对简单的参数做验证后,查看用户是否绑定了支付宝账户信息,以及用户的提现的钱是否符合够用(项目里是最低标准是5元)。

/**  * 提现申请  *@return*/@ResponseBody@RequestMapping("applicationWithdraw")publicObject applicationWithdraw(Integer pid, BigDecimal money){//验证参数if(ObjectUtil.isPassInteger(pid) && money !=null&& money.compareTo(new BigDecimal(5)) !=-1){if(aliacount_dao.selectByPersionId(pid).size()!=0){//用户已绑定支付宝Persion persion = persion_dao.queryPersionByIdForIM(pid);if(money.compareTo(new BigDecimal(persion.getCapitalBalance())) !=1){// 申请提现业务boolean flag = wiService.insertApplication(persion, money);returnflag==true?"success":"error";              }else{return"error";//用户金额不足}          }else{//用户未绑定支付宝return"fail";          }      }else{return"error";      }  }

下面这块给拼接了一个流水号,在后面会有介绍;然后转嫁手续费到用户身上,插记录,扣钱;以事务的方式进行控制。关于用户手续费方面,其实还可以更加灵活一些,无非就是多提少收,少提多收;这个也得业主考量了。

**  * 申请提现业务  *@return*/@Transactionalpublic boolean insertApplication(Persion persion,BigDecimal money){StringonceId = UUIDHashCode.getOrderIdByUUId();//获得一个随机唯一标识//扣除支付宝的手续费 0.5% 最高不会超过25元 最低不会超过1元BigDecimal platform_cost =  money.multiply(newBigDecimal(0.005)).setScale(2, BigDecimal.ROUND_HALF_UP);if(platform_cost.compareTo(newBigDecimal(25)) ==1){          platform_cost =newBigDecimal(25);      }elseif(platform_cost.compareTo(newBigDecimal(1)) ==-1){          platform_cost =newBigDecimal(1);      }      BigDecimal getMoney = money.subtract(platform_cost);//扣除手续费之后的金额intnum=0;if(getMoney.compareTo(newBigDecimal(0)) ==1){// 新增一条提现申请记录num= withMa.insertSelective(newWithdraw(onceId,persion.getId(),getMoney,1,platform_cost));      }if(num==1){// 修改该用户信息中可提现金额信息persion.setCapitalBalance(money.doubleValue());          Persion queryPersion = perMa.queryPersionByIdForIM(persion.getId());if(queryPersion !=null){              persion.setVersion(queryPersion.getVersion());intwithdr = perMa.cutPersionCap(persion);returnwithdr>0?true:false;          }else{num=1/0;returnfalse;          }      }else{num=1/0;returnfalse;      }  }

后台进行处理

因为这个提现还是需要人工操作的,比如输支付密码什么的,支付宝的考量也可能是为了安全吧,所以需要有后台的一系列逻辑。像App支付一样,先从基本信息配置入手。

AlipayConfig这里关键在于私钥和公钥,可以看一看阮一峰的数字签名是什么支付宝App支付的公钥和这里提现所用的公钥是不一致的,对应的RSA的与RSA2的公钥也是不同的。之前不了解公钥的意义,这里的公钥和App支付的共用了,也算是无知的坑吧。商户安全校验码就是上面让保存的那个MD5的key值。

publicclassAlipayConfig{//↓↓↓↓↓↓↓↓↓↓请在这里配置您的基本信息↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓// 合作身份者ID,以2088开头由16位纯数字组成的字符串publicstaticStringpartner ="2088xxxxxxx";//商户支付宝账号publicstaticStringseller_email ="";//商户真实姓名publicstaticStringaccount_name =" XXXXX科技有限公司";// 支付宝用于提现的公钥,一般情况下无需修改该值  (提现),也可与支付宝map网关产品密钥的支付宝公钥做对比,应该是一致的。publicstaticStringali_public_key ="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnxj/9qwVfgoUh/y2W89L6BkRAFljhNhgPdyPuBV64bfQNN1PjbCzkIM6qRdKBoLPXmKKMiFYnkd6rAoprih3/PrQEB/VsW8OoM8fxn67UDYuyBTqA23MML9q1+ilIZwBC2AQ2UBVOrFXfFl75p6/B5KsiNG9zpgmLCUYuLkxpLQIDAQAB";//商户安全校验码publicstaticStringkey ="MD5key";//支付宝异步通知地址    需http://格式的完整路径,不允许加?id=123这类自定义参数publicstaticStringnotify_url = PathUtil.GetDemain() +"/WoBanAdmin/ALiPay/TransNotify";//public static String notify_url = "http://xiaofanfight.viphk.ngrok.org/WoBanAdmin/ALiPay/TransNotify";//↑↑↑↑↑↑↑↑↑↑请在这里配置您的基本信息↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑// 调试用,创建TXT日志文件夹路径publicstaticStringlog_path ="D:\\";// 字符编码格式 目前支持 gbk 或 utf-8publicstaticStringinput_charset ="utf-8";// 签名方式 不需修改(退款签名方式)publicstaticStringsign_type ="MD5";}

代码里有很多是运用支付宝提供的Demo,支付宝并没有对这个接口提供jar包公司的前辈也对此进行了开创性的研究,而我只是完善了一下,并写成了拙劣的文档。

后台的流程及页面展示:

页面是位于pages - recharge 里的。

提现申请列表 money_drawing.jsp

这里每进一次页面会有提示,因为很重要,所以每次都提醒;提示框的蓝色按钮可以跳往支付宝对证书的提示,它会说,目前只支持IE 32位部分浏览器以及UC浏览器,Mac下只支持safari;但我实际测试的时候,只支持火狐......支付宝需要证书是为了后面你输入支付宝支付密码时候的安全。

提现申请详情 BalanceDetail.jsp

一个的时候展示一个的详情,多人的时候展示多人的信息,点击结算后的页面是以弹出层展示的

点击结算后 HandleClearing.jsp

这里的话其实不加验证也可以的,毕竟支付宝那边还要验证支付密码,或者这里也可以结合一下管理员的手机号,进行处理。

输入密码,点击确定后,就走Controller了,从控制层里拼接一些参数,跳往支付宝页面。下面详述ALiPayController

实际上,在上一张图我们可知,能传过来的数据无非就几个:

1.哪个管理员操作的;

2.哪些人申请的提现。

因为像申请的钱什么的是依赖表的而不是页面上的数据。

参数ids实际上控制着三个选择,全部处理、按照选择处理、单个处理;然后一些基础信息的取得,计算提现的总额(依靠SQL),最为关键就是拼接参数,按照文档进行,参数中最为关键的就是付款详细数据以及批次号流水号,批次号每次都是随机生成的,因为支付宝将相同的批次号视作同一笔申请,而流水号在App端提交的时候就做了处理;这里不需要关注一些细节,在支付宝提供的Demo里有涉及,有兴趣可以点进去看一看。

/**

*

* Method ALiPayTrans

* 方法作用:请求批量转账前一步

* 适用条件:提现

* 使用方法:请参照最新的支付宝批量转账有密接口文档

* @param aid    管理员id

* @param ids      提现申请人id的集合

* @param out      用于给支付宝回馈(大概吧)

* @since Met 2.0

*/@RequestMapping(value ="ALiPayTrans", method = RequestMethod.GET)publicvoidALiPayTrans(Integer aid,Stringids,PrintWriter out) {      DecimalFormat df =newDecimalFormat(".00");//将double类型的数据保留两位数List wlist =newArrayList();if(ids!=null && !ids.equals("")){if(ids.equals("all")){//全部处理wlist = withdrawMapper.selectWithdraw(newWithdraw());          }else{if(ids.contains(",")){StringIds=ids.trim().substring(0, ids.length() -1);                  Withdraw withdraw =newWithdraw();                  withdraw.setId(0);                  withdraw.setBatchNo(Ids);                  wlist = withdrawMapper.selectWithdraw(withdraw);              }else{//单个流水wlist = withdrawMapper.selectWithdraw(newWithdraw(Integer.parseInt(ids)));              }          }//服务器异步通知页面路径  通知提现成功与失败Stringnotify_url = AlipayConfig.notify_url;//付款账号Stringemail = AlipayConfig.seller_email;//付款账户名Stringaccount_name = AlipayConfig.account_name;//必填,个人支付宝账号是真实姓名公司支付宝账号是公司名称//付款当天日期Stringpay_date = DateUtil.getDays();//必填,格式:年[4位]月[2位]日[2位],如:20100801//批次号Stringbatch_no = DateUtil.getDays() + DateUtil.getThree() + DateUtil.getThree();//必填,格式:当天日期[8位]+序列号[3至16位],如:201008010000001//付款总金额//计算总额BigDecimal summoney = withdrawMapper.querySumByGet(wlist);if(summoney != null && summoney.compareTo(newBigDecimal(10000000)) ==-1){Stringbatch_fee = df.format(summoney).toString();//必填,即参数detail_data的值中所有金额的总和//付款笔数Integer num = wlist.size();Stringbatch_num = num.toString();//必填,即参数detail_data的值中,“|”字符出现的数量加1,最大支持1000笔(即“|”字符出现的数量999个)/*

* 下列付款详细数据说明以及示例

* String detail_data = batch_no + "^" + "zhangsan@qq.com" + "^" + "张三" + "^" + batch_fee + "^备注说明";

* 解释:其中batch_no为上面生成的转账批次号;zhangsan@qq.com为需要转账的支付宝账户;张三为转账支付宝账户的真实姓名;

* batch_fee为转账金额,最后的参数为附加参数,可以对本次转账备注说明,只要是字符串就可以,但长度不宜过长。请根据需要以此替换

*///必填,即参数detail_data的值中,“|”字符出现的数量加1,最大支持1000笔(即“|”字符出现的数量999个)//付款详细数据Stringdetail_data ="";for(inti=0;i sParaTemp =newHashMap();              sParaTemp.put("service","batch_trans_notify");              sParaTemp.put("partner", AlipayConfig.partner);              sParaTemp.put("_input_charset", AlipayConfig.input_charset);              sParaTemp.put("notify_url", notify_url);              sParaTemp.put("email", email);              sParaTemp.put("account_name", account_name);              sParaTemp.put("pay_date", pay_date);              sParaTemp.put("batch_no", batch_no);              sParaTemp.put("batch_fee", batch_fee);              sParaTemp.put("batch_num", batch_num);              sParaTemp.put("detail_data", detail_data);//建立请求//Log log =newLog(aid,"处理提现申请操作,处理额度为"+ summoney +"元");              logMa.insert(log);StringsHtmlText = AlipaySubmit.buildRequest(sParaTemp,"post","确认");              out.println(sHtmlText);          }else{              out.print("alert('处理批次额度超限。最高不超过1000万元。');");          }      }  }

顺利就跳到了以下的页面,值得注意的是,在这个Controller内,将这笔提现申请改成了处理中,避免其他人会重复处理。请注意这个状态。

支付宝页面

支付宝提示当前操作环境不支持支付宝控件,因为我是谷歌浏览器打开的,这种情况支付宝不会给异步通知,因为它不知道是这种问题,它会通知的情况只有两种:

1.支付成功;

2.支付失败,给转账的支付宝用户信息不正确。

所以处理中的作用就是存放意外情况下的申请,管理员可以在合适的环境下,将处理中的申请再次转化为申请中,再次提交。

申请中列表

支付失败之一 收款支付宝账户信息校验不通过

以下为出现意外的情况截图:

不会通知的情况 未安装证书

上一步安装了之后有刷新选项,但是支付宝会这样说,也不需要管,因为刚刚那笔订单已经在提现申请中里了

不会通知的情况 安装了证书之后未安装电子证书

正常情况下:

安装了电子证书 正常的情况1

下面这个图就说明了,支付宝会给打款成功的回调。

安装了电子证书 正常的情况2

支付宝的异步通知

刚刚问了半天客服,客服也无法说清楚,建议是在IE8 32位浏览器下进行的,可是IE8会和layui会有冲突,这就有点尴尬了。以上的截图是在火狐下进行的。客服也不置可否。

完成了支付宝付款的逻辑,就剩下接收回调的处理了,也在Controller层,对于成功走成功逻辑,失败走失败逻辑。

这里关注一下支付宝的说明即可:如果成功的信息为空,证明都失败了,反之;如果两者都不为空,就需要各自走各自的逻辑了,根据流水号查出提现的详情,成功推送信息,失败返回资金并推送

回调处理的关键

@RequestMapping(value ="TransNotify", method = RequestMethod.POST)/*** 批量付款数据中转账成功的详细信息 String success_details* 批量付款数据中转账失败的详细信息 String fail_details* 批量付款数据中转账批次号 String batch_no*/publicvoidTransNotify(HttpServletRequest request,Stringsuccess_details,Stringbatch_no,Stringfail_details, PrintWriter out) {//获取支付宝POST过来反馈信息Map params = GetInfoFromALiPay(request);        boolean flag =true;if(AlipayNotify.verify(params)){//验证成功//判断是否在商户网站中已经做过了这次通知返回的处理;如果没有做过处理,那么执行商户的业务程序;如果有做过处理,那么不执行商户的业务程序//可以判断success_details是否为null来标识转账是否成功,支付宝方面明确说明如果转账成功success_details不为null,fail_details则//为null;若转账失败success_details为null而fail_details不为null,同样根据batch_no来查询转账对象并更新转账状态if(fail_details ==null){//提现全部成功 处理相关业务 看是否已经处理过了 改状态  推送withdrawService.getMoneySuccess(batch_no);            }elseif(success_details ==null){//提现全部失败//返还未提现用户的金币 推送Stringinfo = withdrawService.getMoneyError(batch_no);if("error".equals(info)){                    flag =false;//自己sql出错,请求支付宝再次发送验证}            }else{//转账部分成功/部分失败Stringinfo = withdrawService.getMoneySuccessOrError(batch_no, success_details, fail_details);if("error".equals(info)){                    flag =false;//自己sql出错,请求支付宝再次发送验证}            }if(flag){                out.println("success");//请勿修改该值!}else{                out.println("fail");//自己sql出错,请求支付宝再次发送验证}        }else{//验证失败//程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是success这7个字符,支付宝服务器会不断重发通知,直到超过24小时22分钟out.println("fail");        }    }@SuppressWarnings("unchecked")    publicMap GetInfoFromALiPay(HttpServletRequest request) {Map params =newHashMap();MaprequestParams = request.getParameterMap();for(Iteratoriter = requestParams.keySet().iterator(); iter.hasNext();) {Stringname = (String) iter.next();String[] values = (String[]) requestParams.get(name);StringvalueStr ="";for(inti =0; i < values.length; i++) {                valueStr = (i == values.length -1) ? valueStr + values[i] : valueStr + values[i]                        +",";            }//乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化//valueStr = new String(valueStr.getBytes("ISO-8859-1"), "gbk");params.put(name, valueStr);        }returnparams;    }

5. 结束

第一次总结,文字还是很冗余的。在这里还是需要感谢一下ydm公司的前辈。写于2017-03-17。

推荐阅读更多精彩内容