电商项目实战之支付宝支付订单

电商项目实战之支付宝支付订单

支付加密

  • 加密流程介绍

    1. 支付宝加密采用RSA非对称加密,分别在商户端和支付宝端有两对公钥和私钥

    2. 发送订单数据时,直接使用明文,但会使用商户私钥加一个对应的签名支付宝端使用商户公钥对签名进行验签,只有数据明文和签名对应的时候才能说明传输正确

    3. 支付成功后,支付宝发送支付成功数据之外,还会使用支付宝私钥加一个对应的签名商户端收到支付成功数据之后也会使用支付宝公钥验签,成功后才能确认。

  • 加密流程图示

环境配置

配置支付宝沙箱环境

  • 后续更新

项目搭建

  • 导入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";
        }
    
    

异步通知

  • 异步通知流程

    1. 订单支付成功后支付宝会回调商户接口,这个时候需要修改订单状态

    2. 由于同步跳转可能由于网络问题失败,所以使用异步通知;

    3. 支付宝使用的是最大努力通知方案,保障数据一致性,隔一段时间会通知商户支付成功,直到返回success

  • 内网穿透配置

    1. 外网映射到本地的order.gulimall.com:80;

    2. 由于回调的请求头不是order.gulimall.com,因此nginx转发到网关后找不到对应的服务,所以需要对nginx进行设置

    3. 将/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

上一篇:uni-app 开发钉钉小程序


下一篇:支付宝支付(三):周期扣款实现注意细节