支付宝沙箱开发使用
1. Order表设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 drop table if exists edu_order;create table edu_order( id char (19 ) not null comment '订单ID' , trade_no char (40 ) null comment '支付宝交易号' , course_id char (19 ) not null comment '课程ID' , ucenter_id char (19 ) not null comment '用户ID' , ucenter_username varchar (50 ) default null comment '用户名' , ucenter_mobile varchar (20 ) not null comment '用户手机号' , course_title varchar (50 ) not null comment '课程标题' , course_price decimal (10 ,2 ) unsigned not null default '0.00' comment '课程价格' , trade_status int (2 ) not null default 0 comment '交易状态:WAIT_BUYER_PAY(交易创建,等待买家付款)0、TRADE_CLOSED(未付款交易超时关闭,或支付完成后全额退款)1、TRADE_SUCCESS(交易支付成功)2、TRADE_FINISHED(交易结束,不可退款)3' , gmt_create datetime not null comment '创建时间' , gmt_modified datetime not null comment '更新时间' , primary key(id) ) select * from edu_order;
支付宝扫码登录后,进入网页和应用开发,创建一个新应用
这里需要下载一个开发助手,用于生成公钥和私钥
支付宝采用公钥+内容匹配私钥的方式
生成密钥 https://opendocs.alipay.com/open/291/105971
复制公钥到指定位置后等待验审上线,但应用的签约功能需要营业执照。
选用支付宝沙箱开发
沙箱环境https://openhome.alipay.com/platform/appDaily.htm?tab=info
4. 沙箱环境 沙箱环境是开放平台提供给开发者调试接口的环境,模拟真实的支付环境
配置信息 https://opendocs.alipay.com/open/200/105311
5. 支付接入 配置说明 https://opendocs.alipay.com/open/270/105899
5.1. 支付时序图
5.2. 下单实现 5.2.1. controller 通过请求中有的logintoken取得用户头像,id,昵称信息,并根据课程信息生成订单
返回orderId是为了支付的时候接收订单号
1 2 3 4 5 6 7 8 9 10 @PostMapping("/create") public R createOrder (HttpServletRequest request, @RequestBody EduOrder eduOrder) { String token = request.getHeader("logintoken" ); String orderId=eduOrderService.createOrder(token,eduOrder); if (!StringUtils.isEmpty(orderId)) { return R.ok().message("订单创建成功,及时付款" ).data("orderId" ,orderId); } return R.error().message("订单创建失败,请稍后再试" ); }
5.2.2. service 将下单信息通过rabbit中的order-queue队列,实现分布式事务
memberClient 来自SpringCloud中的openfeign,调用用户表中的路由得到手机号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public String createOrder (String token, EduOrder eduOrder) { Claims claims = JwtUtils.checkJWT(token); String ucenterId = (String)claims.get("id" ); String ucenterName = (String) claims.get("nickname" ); if (StringUtils.isEmpty(ucenterId)){ return null ; } R result = memberClient.getMember(ucenterId); Map<String,Object> data = result.getData(); String mobile = (String)data.get("mobile" ); eduOrder.setUcenterId(ucenterId); eduOrder.setUcenterUsername(ucenterName); eduOrder.setUcenterMobile(mobile); baseMapper.insert(eduOrder); rabbitTemplate.convertAndSend("order-exchange" ,"order-binding" ,eduOrder); return eduOrder.getId(); }
5.3. Maven依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependency > <groupId > com.alipay.sdk</groupId > <artifactId > alipay-sdk-java</artifactId > <version > 4.9.79.ALL</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-amqp</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
5.4. 支付实现 5.4.1. 发起支付请求 5.4.1.1. application.properties中相关配置 这里的回调地址和返回地址都必须是公网域名,通过ngrok,花生壳等注册域名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 server.port =8008 spring.application.name =edu-alipay eureka.instance.prefer-ip-address =true eureka.client.service-url.defaultZone =http://127.0.0.1:8010/eureka spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/online spring.datasource.username =<<账号>> spring.datasource.password =<<密码>> alipay.config.APP_ID = <<沙箱中的app_id>> alipay.config.APP_PRIVATE_KEY = <<私钥>> alipay.config.GATEWAY_URL =https://openapi.alipaydev.com/gateway.do alipay.config.SIGN_TYPE =RSA2 alipay.config.CHARSET =utf-8 alipay.config.FORMAT =json alipay.config.ALIPAY_PUBLIC_KEY =alipay.config.NOTIFY_URL =http://<<内网穿透注册的域名>>/alipay/edu-order/payback alipay.config.RETURN_URL =http://<<内网穿透注册的域名>>/alipay/edu-order/return alipay.config.SERVICE_PROVIDER_ID =<<沙箱中卖家app_id>> spring.rabbitmq.host =192.168.0.112 spring.rabbitmq.port =5672 spring.rabbitmq.listener.direct.acknowledge-mode =manual spring.jackson.date-format =yyyy-MM-dd HH:mm:ss spring.jackson.time-zone =GMT+8 mybatis-plus.configuration.log-impl =org.apache.ibatis.logging.stdout.StdOutImpl mybatis-plus.mapper-locations =classpath:com/online/edu/alipay/mapper/xml/*.xml
5.4.1.2. config 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.online.edu.alipay.config;import com.alipay.api.AlipayClient;import com.alipay.api.DefaultAlipayClient;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;@Data @Configuration @ConfigurationProperties(prefix = "alipay.config") public class AlipayConfig { private String APP_ID; private String APP_PRIVATE_KEY; private String CHARSET; private String ALIPAY_PUBLIC_KEY; private String GATEWAY_URL; private String FORMAT; private String SIGN_TYPE; private String NOTIFY_URL; private String RETURN_URL; private String SERVICE_PROVIDER_ID; public AlipayClient getClient () { AlipayClient alipayClient = new DefaultAlipayClient ( this .getGATEWAY_URL() , this .getAPP_ID(), this .getAPP_PRIVATE_KEY(), this .getFORMAT(), this .getCHARSET(), this .getALIPAY_PUBLIC_KEY(), this .getSIGN_TYPE()); return alipayClient; } }
5.4.1.3. controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package com.online.edu.alipay.controller;import com.online.edu.alipay.config.AlipayConfig;import com.online.edu.alipay.entity.Query.FileVo;import com.online.edu.alipay.service.PayService;import com.online.edu.common.R;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@Controller @CrossOrigin @RequestMapping("/alipay/edu-order") public class AlipayController { @Autowired PayService payService; @Autowired AlipayConfig alipayConfig; @PostMapping("/pay") public void pay (@RequestParam("orderId") String orderId, HttpServletResponse response) throws IOException { payService.createForm(orderId,response); } @PostMapping("/payback") @ResponseBody public R payBack (HttpServletRequest request, HttpServletResponse response) { boolean flag = payService.notify(request,response); if (flag){ return R.ok().message("验签成功" ); }else { return R.error().message("验签失败" ); } } @GetMapping("/return") @ResponseBody public void returnUrl (HttpServletRequest request,HttpServletResponse response) throws IOException { String courseId = payService.backtToCourseId(request); response.sendRedirect("http://localhost:3000/course/" +courseId); } }
5.4.1.4. service 5.4.1.4.1. 发起请求 这里的rabbit最终会将信息放到死信队列中,得到30分钟交易关闭后的交易订单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Override public void createForm (String orderId, HttpServletResponse response) { EduOrder eduOrder = eduOrderService.getById(orderId); BigDecimal coursePrice = eduOrder.getCoursePrice(); String courseTitle=eduOrder.getCourseTitle(); AlipayClient alipayClient = alipayConfig.getClient(); AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest (); alipayRequest.setReturnUrl(alipayConfig.getRETURN_URL() ); alipayRequest.setNotifyUrl(alipayConfig.getNOTIFY_URL()); String SERVICE_PROVIDER_ID = alipayConfig.getSERVICE_PROVIDER_ID(); alipayRequest.setBizContent( "{" + " \"out_trade_no\":\"" +orderId+"\"," + " \"product_code\":\"FAST_INSTANT_TRADE_PAY\"," + " \"total_amount\":" +coursePrice+"," + " \"subject\":\"" +courseTitle+"\"," + " \"body\":\"" +courseTitle+"\"," + " \"timeout_express\":\"30m\"," + " \"extend_params\":{" + " \"sys_service_provider_id\":\"" +SERVICE_PROVIDER_ID+"\"" + " }" + " }" ); String form= "" ; try { form = alipayClient.pageExecute(alipayRequest).getBody(); response.setContentType( "text/html;charset=" + alipayConfig.getCHARSET()); response.getWriter().write(form); response.getWriter().flush(); response.getWriter().close(); rabbitTemplate.convertAndSend("user.order.delay.exchange" ,"user.order.delay.queue" ,orderId); } catch (AlipayApiException e) { log.error("getAliPayOrderStr error :{}" , e); } catch (IOException e){ log.error("Alipay response error :{}" , e); } }
5.4.1.4.2. 回调 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 @Override public boolean notify (HttpServletRequest request, HttpServletResponse response) { Map<String, String> paramsMap = new HashMap <>(); Map<String, String[]> param = request.getParameterMap(); Set<String> keys = param.keySet(); for (String key:keys){ String[] values = param.get(key); String value = "" ; for (int i = 0 ; i < values.length; i++) { value = (i == values.length - 1 ) ? value + values[i] : value + values[i] + "," ; } paramsMap.put(key,value); } try { boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, alipayConfig.getALIPAY_PUBLIC_KEY(), alipayConfig.getCHARSET(), alipayConfig.getSIGN_TYPE()); if (signVerified){ log.info("订单创建时间戳:{} 商户订单号:{} 交易金额:{} 支付宝交易号:{} 收款支付宝账号:{}" , paramsMap.get("gmt_create" ), paramsMap.get("out_trade_no" ), paramsMap.get("total_amount" ), paramsMap.get("trade_no" ), paramsMap.get("seller_id" )); String orderId = paramsMap.get("out_trade_no" ); String trade_no = paramsMap.get("trade_no" ); String status = paramsMap.get("trade_status" ); Integer resultStatus = new StatusUtils ().getStatus(status); EduOrder order = eduOrderService.getById(orderId); order.setTradeNo(trade_no); order.setTradeStatus(resultStatus); eduOrderService.updateById(order); return true ; } } catch (AlipayApiException e) { log.error("getAliPayOrderStr error :{}" , e); } return false ; }
返回参数列表
参数定义
https://opendocs.alipay.com/apis/api_1/alipay.trade.page.pay
5.4.1.4.3. 成功返回到课程页 1 2 3 4 5 6 @Override public String backtToCourseId (HttpServletRequest request) { String orderId = request.getParameter("out_trade_no" ); EduOrder order = eduOrderService.getById(orderId); return order.getCourseId(); }
5.4.2. 支付过程
5.4.3. 账单下载 https://opendocs.alipay.com/apis/api_15/alipay.data.dataservice.bill.downloadurl.query
传入的是文件要保存的路径和要下载的时间日期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 @Override public String getMonthInfo (FileVo file) { AlipayClient alipayClient = alipayConfig.getClient(); AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest (); AlipayDataDataserviceBillDownloadurlQueryModel model = new AlipayDataDataserviceBillDownloadurlQueryModel (); model.setBillDate(file.getDate()); model.setBillType("trade" ); request.setBizModel(model); AlipayDataDataserviceBillDownloadurlQueryResponse response = null ; try { response = alipayClient.execute(request); } catch (AlipayApiException e) { log.error("Alipay response error :{}" , e); } log.info("response body: {}" ,response.getBody()); String urlStr = response.getBillDownloadUrl(); String filePath = file.getPath(); File outFile = new File (filePath); outFile.mkdirs(); URL url = null ; HttpURLConnection httpUrlConnection = null ; InputStream fis = null ; FileOutputStream fos = null ; try { url = new URL (urlStr); httpUrlConnection = (HttpURLConnection) url.openConnection(); httpUrlConnection.setConnectTimeout( 5 * 1000 ); httpUrlConnection.setDoInput( true ); httpUrlConnection.setDoOutput( true ); httpUrlConnection.setUseCaches( false ); httpUrlConnection.setRequestMethod( "GET" ); httpUrlConnection.setRequestProperty( "Charsert" , "UTF-8" ); httpUrlConnection.connect(); fis = httpUrlConnection.getInputStream(); byte [] temp = new byte [ 1024 ]; int b; fos = new FileOutputStream (new File (outFile,file.getDate()+".csv.zip" )); while ((b = fis.read(temp)) != - 1 ) { fos.write(temp, 0 , b); fos.flush(); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { throw new EduException (20001 ,"文件路径不存在" ); } finally { try { if (fis!= null ) fis.close(); if (fos!= null ) fos.close(); if (httpUrlConnection!= null ) httpUrlConnection.disconnect(); } catch (IOException e) { e.printStackTrace(); } } return filePath+"/" +file.getDate()+".csv.zip" ; }
5.4.3.1. Error 1 {"alipay_data_dataservice_bill_downloadurl_query_response":{"code":"40004","msg":"Business Failed","sub_code":"isv.invalid_arguments","sub_msg":"入参不合法"},"sign":"PDmYgxlhULgz/pPFT/qCU6XafHNhwLqbGYQ+NhBeqlg34UTU6toOTNCuocfGJ/sJn3lO2bO0eqMVE3PGrwRWH+QkuoJ3hSKXylfroJCkus5FZhWi5ErczVnIWRXy49t/MIOwOaBXWYzZTJb86aNyBYDurXS+t7LVr6SA7yDDxNsdksHC3IDLWqcM76ox4fKlrtTOpljrsZAMnubBk0WCMju3NT2kD6h3cMirdXGI7WJX8Fw70o19cM9+qScQTFjobaXWD3o8NrFCJHd9drUt5NRBymmkHAyR5W4CXW3JaiksLGeUABXAXgApf48mdNWxFMCLi2BDhFIwCCLA+BYu7g=="}2019-10-06 14:06:00.611 INFO 1010 --- [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Disable delta property : false
时间不合适,只有得到今日之前的账单
5.4.4. 得到账单信息 传入 订单ID 和 支付交易号
https://opendocs.alipay.com/apis/api_1/alipay.trade.query
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public String getPayInfoById (String orderId) { EduOrder order = eduOrderService.getById(orderId); AlipayClient alipayClient = alipayConfig.getClient(); AlipayTradeQueryRequest request = new AlipayTradeQueryRequest (); AlipayTradeQueryModel model = new AlipayTradeQueryModel (); model.setOutTradeNo(orderId); model.setTradeNo(order.getTradeNo()); request.setBizModel(model); AlipayTradeQueryResponse response = null ; try { response=alipayClient.execute(request); return response.getBody(); } catch (AlipayApiException e) { log.error("alipay error: {}" ,e); } return null ; }
5.4.5. 超时后支付url https://excashier.alipaydev.com/standard/auth.htm?payOrderId=107792034adb49d8958183b198f7c817.00
https://excashier.alipaydev.com/standard/timeOutPage.htm?payOrderId=1112dd699d4a468e9bf5fca83477178b.00