电商项目实战之支付宝支付订单
支付加密
-
加密流程介绍
-
支付宝加密采用RSA非对称加密,分别在商户端和支付宝端有两对公钥和私钥;
-
在发送订单数据时,直接使用明文,但会使用商户私钥加一个对应的签名,支付宝端会使用商户公钥对签名进行验签,只有数据明文和签名对应的时候才能说明传输正确;
-
支付成功后,支付宝发送支付成功数据之外,还会使用支付宝私钥加一个对应的签名,商户端收到支付成功数据之后也会使用支付宝公钥验签,成功后才能确认。
-
-
加密流程图示
环境配置
配置支付宝沙箱环境
- 后续更新
项目搭建
-
导入pom依赖
<!-- 支付宝sdk --> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.9.28.ALL</version> </dependency>
-
支付工具类
成功调用支付接口后,返回的数据是支付页面的html,因此后续会使用@ResponseBody
import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayClient; import com.alipay.api.DefaultAlipayClient; import com.alipay.api.request.AlipayTradePagePayRequest; import com.xunqi.gulimall.order.vo.PayVo; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "alipay") @Component @Data public class AlipayTemplate { // 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号 public String app_id; // 商户私钥,您的PKCS8格式RSA2私钥 public String merchant_private_key; // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。 public String alipay_public_key; // 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息 public String notify_url; // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 //同步通知,支付成功,一般跳转到成功页 public String return_url; // 签名方式 private String sign_type; // 字符编码格式 private String charset; //订单超时时间 private String timeout = "1m"; // 支付宝网关; https://openapi.alipaydev.com/gateway.do public String gatewayUrl; public String pay(PayVo vo) throws AlipayApiException { //AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type); //1、根据支付宝的配置生成一个支付客户端 AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl, app_id, merchant_private_key, "json", charset, alipay_public_key, sign_type); //2、创建一个支付请求 //设置请求参数 AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest(); alipayRequest.setReturnUrl(return_url); alipayRequest.setNotifyUrl(notify_url); //商户订单号,商户网站订单系统中唯一订单号,必填 String out_trade_no = vo.getOut_trade_no(); //付款金额,必填 String total_amount = vo.getTotal_amount(); //订单名称,必填 String subject = vo.getSubject(); //商品描述,可空 String body = vo.getBody(); alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\"," + "\"total_amount\":\""+ total_amount +"\"," + "\"subject\":\""+ subject +"\"," + "\"body\":\""+ body +"\"," + "\"timeout_express\":\""+timeout+"\"," + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}"); String result = alipayClient.pageExecute(alipayRequest).getBody(); //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面 System.out.println("支付宝的响应:"+result); return result; } }
-
支付宝配置
#支付宝相关的配置 alipay.app_id=2021000116682294 alipay.merchant_private_key=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqEQu7M6ALRdlY38omxoPRrSGY+o5BnJB+jdbHsVqgRw4dyfaGoYEnc8hZDwMTBxOlgSuXCIXjS7L/8jHpFpnyysQ66prSrdWOOfn0Y3BWeCeM7O/hYV8nWQEtU4NQ8Gbvzi8Nc1OYRzPsP81pJbCgegMCcO2XU+ETGynOwybwTEPW6kRYr3tlUh4gWLYD6FNRgyEaYLebxEcEta3GVJ+bnBVj4/T0XFDbijBfwLkIb3lkRBpZ9qZKMr2whq5xZU5da51zA1OXrithU9J5PCvBsfYPZzD2XB7w/FJwAZHiaf/MZDQHgs7obk34wJdIEU+6PsqeBluitsSPnaGqHe+/AgMBAAECggEARCGGEJ4S/NEjzK4C31viDUsNzap6+SAO5xRNujx78P/mUNrLL45eHn8NAVi5Q5MvNLu1ydD5SmDWOiE+C4IpdJH04SGBmutXRZ5GW7jGlW2XcqatRx5qL2pBxXXSgsd8hY+VXA8aq0PDMl8xHHGj+/ZFZGH3XQuWfMACFLorzFIoHTRNLYnRieG4ugwKoDIcuscwsMQPWmngDGiwZ4UHF2QVzvdBi0+LFDQBUgmulz78AESCBdz2AaT2cN6MAyjBWfC/vjnTL4lDa/RAKF3+vztQFbPwU91caNJ7ZRQbdM97YFA05/TbtZHVISVfsrtEV7aJabRFtbKv1JHc9faVUQKBgQDzpWVG3+h1IGYhmrH3JN+hy+JqI7r1xoZuUDEhSwnwgUvsUbKfi4kq/9upPP+UwTJkzosmoXm++eJ0NJK7u9GkNvDe2NAVuKC2yUXQcVZH3g66nK8RA77P0gke3zxFvMAvwNcNbBlBxWpnq18um36xNL3vH/dwvnXOPjTgsu0QxwKBgQCysJCUa9Qq3alG8zhh8ki3UxnyuZAZn0Aun73xMVbsuNXeD05RJJl6v6htvrbMzpl5VaWVrFlBxe86wLa3SF+LChNBbOTI8wHrxcI0zjjgM4hnyxkGc7bmydu11jgB1FW2bDjdef/jXcHBR5zv9iuGtf5u44P6calnlluu0lGhSQKBgQDOzrkka7IYmSOXqoMc0IOyBiltBl8Pdy9sO004gUUyi80yDjacDgikIwEEqe4XCdYIkviyFaYNYHXDRUy3ZTkRNkFGJqqZhVmFTVqhD1K5NdcytwQyYQd5x0JOnhW+6/QFlJ31mqfll/g1ftUP7pSfPbFqY6vYWxILuemQQM/QyQKBgBEVpJ0sOhrnGpo4AvoKaj1s6pw0dbydZ0uN5mE7MG5ttbUHjZtgfnQoAviLNXsbfb0GHVDUK6yIOjlJ165qcwgeLH0i5jT4VwSt4cWeuf1lr/MMjhX51lD2l9Uo6C30mUVZz44NbhPVEQNjz3N5++tjVzJO+n1lhKbsz1NFtJxRAoGAeWhJfWZ4QAvJHxYxA4b8h8TeNqVctbRY9Csf5s1HVsHWCmgzvkkPT++uu/gM3fQTqDjIIgjrPyaaZ+zcwFAVGw4axhl4qBkJp8tJBBu94uQxd++nOvmWcCLPQPnYElz6sgxzHFor5PgY8Ab6l24V3a/E1AkPqnm+MloRrZZT4EE= alipay.alipay_public_key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnuiCm8A+CYa5M6w6BzpWFQpLbTlAWNuaLsDecsnqbJE69Ara5+w5pGa9B/Q/do5zeKi91Ai++FudyZCE5Dhv2zmNK1cnxF6fGlWq1io/8KpAlHJLR1McNny128N9BLx6KtwldC0R5zs4ebjwZcxbmrkQL7prF1wKzBGo2f80PyNr8Me8MTUE3sE+p4gc4/eEn01ijou5iGJNMMMMpij7MAtlXTECiEeBX4PMHjXW2cqucKAiln2qi6K77QGwd/Y51fACR89VqJN2/Yg81jr3Lvw3uiXGwLrqrm6ZxM2uLL7HFsx2RNmuddEnHB3vS0kicmS6TsT9hHsRPSyrBbFO2QIDAQAB #alipay.notify_url=http://hjl.mynatapp.cc/payed/notify alipay.notify_url=http://tzy.nat300.top/payed/notify alipay.return_url=http://member.gulimall.com/memberOrder.html alipay.sign_type=RSA2 alipay.charset=utf-8 alipay.gatewayUrl=https://openapi.alipaydev.com/gateway.do
业务代码实现
订单支付
-
点击支付跳转支付接口
/** * 用户下单:支付宝支付 PayWebController * 1、让支付页让浏览器展示 * 2、支付成功以后,跳转到用户的订单列表页 * @param orderSn * @return * @throws AlipayApiException */ @ResponseBody @GetMapping(value = "/aliPayOrder",produces = "text/html") public String aliPayOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException { PayVo payVo = orderService.getOrderPay(orderSn); String pay = alipayTemplate.pay(payVo); System.out.println(pay); return pay; } /** * 获取当前订单的支付信息 OrderServiceImpl * @param orderSn * @return */ @Override public PayVo getOrderPay(String orderSn) { PayVo payVo = new PayVo(); OrderEntity orderInfo = this.getOrderByOrderSn(orderSn); //保留两位小数点,向上取值 BigDecimal payAmount = orderInfo.getPayAmount().setScale(2, BigDecimal.ROUND_UP); payVo.setTotal_amount(payAmount.toString()); payVo.setOut_trade_no(orderInfo.getOrderSn()); //查询订单项的数据 List<OrderItemEntity> orderItemInfo = orderItemService.list( new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn)); OrderItemEntity orderItemEntity = orderItemInfo.get(0); payVo.setBody(orderItemEntity.getSkuAttrsVals()); payVo.setSubject(orderItemEntity.getSkuName()); return payVo; }
同步通知与异步通知
同步通知
-
设置成功回调地址为订单详情页
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 // 同步通知,支付成功,一般跳转到成功页 private String return_url="http://order.gulimall.com/memberOrder.html"; /** * 获取当前用户的所有订单 * @return */ @GetMapping(value = "/memberOrder.html") public String memberOrderPage(@RequestParam(value = "pageNum",required = false,defaultValue = "0") Integer pageNum, Model model, HttpServletRequest request) { //获取到支付宝给我们转来的所有请求数据 //request,验证签名 //查出当前登录用户的所有订单列表数据 Map<String,Object> page = new HashMap<>(); page.put("page",pageNum.toString()); //远程查询订单服务订单数据 R orderInfo = orderFeignService.listWithItem(page); System.out.println(JSON.toJSONString(orderInfo)); model.addAttribute("orders",orderInfo); return "orderList"; }
异步通知
-
异步通知流程
-
订单支付成功后支付宝会回调商户接口,这个时候需要修改订单状态;
-
由于同步跳转可能由于网络问题失败,所以使用异步通知;
-
支付宝使用的是最大努力通知方案,保障数据一致性,隔一段时间会通知商户支付成功,直到返回success。
-
-
内网穿透配置
-
将外网映射到本地的order.gulimall.com:80;
-
由于回调的请求头不是order.gulimall.com,因此nginx转发到网关后找不到对应的服务,所以需要对nginx进行设置;
-
将/payed/notify异步通知转发至订单服务
nginx配置
后续整理
-
-
验证签名
支付宝异步通知各参数详细意义见支付宝开放平台异步通知
https://opendocs.alipay.com/open/194/103296
@RestController public class OrderPayedListener { @Autowired private OrderService orderService; @Autowired private AlipayTemplate alipayTemplate; @PostMapping(value = "/payed/notify") public String handleAlipayed(PayAsyncVo asyncVo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException { // 只要收到支付宝的异步通知,返回 success 支付宝便不再通知 // 获取支付宝POST过来反馈信息 //TODO 需要验签 Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); // 支付宝返回异步通知参数 for (String name : requestParams.keySet()) { String[] values = requestParams.get(name); String valueStr = ""; for (int i = 0; i < values.length; i++) { valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ","; } //乱码解决,这段代码在出现乱码时使用 // valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8"); params.put(name, valueStr); } boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(), alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名 if (signVerified) { System.out.println("签名验证成功..."); //去修改订单状态 String result = orderService.handlePayResult(asyncVo); return result; } else { System.out.println("签名验证失败..."); return "error"; } } @PostMapping(value = "/pay/notify") public String asyncNotify(@RequestBody String notifyData) { //异步通知结果 return orderService.asyncNotify(notifyData); } }
-
修改订单状态并保存交易流水
/** * 处理支付宝的支付结果 * @param asyncVo * @return */ @Transactional(rollbackFor = Exception.class) @Override public String handlePayResult(PayAsyncVo asyncVo) { //保存交易流水信息 PaymentInfoEntity paymentInfo = new PaymentInfoEntity(); paymentInfo.setOrderSn(asyncVo.getOut_trade_no()); paymentInfo.setAlipayTradeNo(asyncVo.getTrade_no()); paymentInfo.setTotalAmount(new BigDecimal(asyncVo.getBuyer_pay_amount())); paymentInfo.setSubject(asyncVo.getBody()); paymentInfo.setPaymentStatus(asyncVo.getTrade_status()); paymentInfo.setCreateTime(new Date()); paymentInfo.setCallbackTime(asyncVo.getNotify_time()); //添加到数据库中 this.paymentInfoService.save(paymentInfo); //修改订单状态 //获取当前状态 String tradeStatus = asyncVo.getTrade_status(); if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) { //支付成功状态 String orderSn = asyncVo.getOut_trade_no(); //获取订单号 this.updateOrderStatus(orderSn,OrderStatusEnum.PAYED.getCode(),PayConstant.ALIPAY); } return "success"; }
收单
-
注意事项
由于可能出现订单已经过期后,库存已经解锁,但支付成功后再修改订单状态的情况,需要设置支付有效时间,只有在有效期内才能进行支付
// timeout - 过期时间 alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\"," + "\"total_amount\":\""+ total_amount +"\"," + "\"subject\":\""+ subject +"\"," + "\"body\":\""+ body +"\"," + "\"timeout_express\":\""+timeout+"\"," + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
超时后订单显示:抱歉您的交易因超时已失败
-
其他情况
订单在支付页,不支付一直刷新,订单过期了才支付,订单状态改为已支付了,但是库存已经解锁
解决方案:使用支付宝自动收单功能解决,只要一段时间不支付,就无法支付。
由于时延等问题,订单解锁完成,正在解锁库存时,异步通知才到(这时早已支付成功)。
解决方案:订单解锁,收到调用收单。
网络阻塞等问题,订单支付成功的异步通知一直未到达。
解决方案:查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝次订单状态。
其他
解决方案:闲时定时任务下载支付宝对账单,一一对账。
参考链接
-
【谷粒商城】分布式事务与下单
https://blog.csdn.net/hancoder/article/details/114983771
-
全网最强电商教程《谷粒商城》对标阿里P6/P7,40-60万年薪
https://www.bilibili.com/video/BV1np4y1C7Yf?p=284
-
mall源码工程
https://github.com/CharlesKai/mall