• 谷粒商城14——订单支付(AliPay)


    十四、订单支付

    使用支付宝来完成

    支付宝开放平台:支付宝开放平台 (alipay.com)

    官方Demo:手机网站支付 DEMO - 支付宝文档中心 (alipay.com)

    官方Demo是用eclipse做的,要用idea导入启动

    image-20220819124515218

    AlipayConfig:

    package com.alipay.config;
    
    public class AlipayConfig {
       // 商户appid
       public static String APPID = "";
       // 私钥 pkcs8格式的
       public static String RSA_PRIVATE_KEY = "";
       // 服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
       public static String notify_url = "http://商户网关地址/alipay.trade.wap.pay-JAVA-UTF-8/notify_url.jsp";
       // 页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址
       public static String return_url = "http://商户网关地址/alipay.trade.wap.pay-JAVA-UTF-8/return_url.jsp";
       // 请求网关地址
       public static String URL = "https://openapi.alipaydev.com/gateway.do";
       // 编码
       public static String CHARSET = "UTF-8";
       // 返回格式
       public static String FORMAT = "json";
       // 支付宝公钥
       public static String ALIPAY_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjrEVFMOSiNJXaRNKicQuQdsREraftDA9Tua3WNZwcpeXeh8Wrt+V9JilLqSa7N7sVqwpvv8zWChgXhX/A96hEg97Oxe6GKUmzaZRNh0cZZ88vpkn5tlgL4mH/dhSr3Ip00kvM4rHq9PwuT4k7z1DpZAf1eghK8Q5BgxL88d0X07m9X96Ijd0yMkXArzD7jg+noqfbztEKoH3kPMRJC2w4ByVdweWUT2PwrlATpZZtYLmtDvUKG/sOkNAIKEMg3Rut1oKWpjyYanzDgS7Cg3awr1KPTl9rHCazk15aNYowmYtVabKwbGVToCAGK+qQ1gT3ELhkGnf3+h53fukNqRH+wIDAQAB";
       // 日志记录目录
       public static String log_path = "/log";
       // RSA2
       public static String SIGNTYPE = "RSA2";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    1.公钥、私钥、加密、加签、验签

    1. 公钥私钥
      • 公钥和私钥是一个相对概念
      • 它们的公私性是相对于生成者来说的。
      • 一对密钥生成后,保存在生成者手里的就是私钥,
      • 生成者发布出去大家用的就是公钥
    2. 加密和数字签名
      • 加密是指:
        • 我们使用一对公私钥中的一个密钥来对数据进行加密,而使用另一个密钥来进行解 密的技术。
        • 公钥和私钥都可以用来加密,也都可以用来解密。
        • 但这个加解密必须是一对密钥之间的互相加解密,否则不能成功。
        • 加密的目的是:
        • 为了确保数据传输过程中的不可读性,就是不想让别人看到。
      • 签名:
        • 给我们将要发送的数据,做上一个唯一签名(类似于指纹)
        • 用来互相验证接收方和发送方的身份;
        • 在验证身份的基础上再验证一下传递的数据是否被篡改过。因此使用数字签名可以 用来达到数据的明文传输。
      • 验签
        • 支付宝为了验证请求的数据是否商户本人发的,
        • 商户为了验证响应的数据是否支付宝发的

    1.1 加密-对称加密

    缺点:

    • 两方任意一方密钥被截取都会造成数据的泄露,以及数据整个流程的控制

    image-20220819124727379

    1.2 加密-非对称加密

    优点:

    • 四把密钥,缺少任何一把都无法模拟完整的流程。

    image-20220819124846831

    支付宝的支付流程:

    image-20220819130841832

    2.沙箱环境

    线上使用阿里支付,需要已备案的域名,所有选择沙箱环境测试

    支付宝开放平台 (alipay.com)里可以直接查看生成的公钥、私钥

    image-20220819131734731

    3.内网穿透

    image-20220819135102144

    1. 简介

      内网穿透功能可以允许我们使用外网的网址来访问主机;

      正常的外网需要访问我们项目的流程是:

      1. 买服务器并且有公网固定 IP
      2. 买域名映射到服务器的 IP
      3. 域名需要进行备案和审核
    2. 使用场景

      1. 开发测试(微信、支付宝)
      2. 智慧互联
      3. 远程控制
      4. 私有云

    用Ngrok内网穿透,网址:ngrok.cc

    4.整合SDK

    下载:概述 - 支付宝文档中心 (alipay.com)

    <dependency>
      <groupId>com.alipay.sdkgroupId>
      <artifactId>alipay-sdk-javaartifactId>
      <version>4.9.28.ALLversion>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    封装工具类:

    package com.henu.soft.merist.gulimall.order.config;
    
    import com.alipay.api.AlipayApiException;
    import com.alipay.api.AlipayClient;
    import com.alipay.api.DefaultAlipayClient;
    import com.alipay.api.request.AlipayTradePagePayRequest;
    import com.henu.soft.merist.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
        private   String app_id = "2016092200568607";
    
        // 商户私钥,您的PKCS8格式RSA2私钥
        private  String merchant_private_key = "";
        // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
        private  String alipay_public_key = "";
        // 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
        // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
        private  String notify_url;
    
        // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
        //同步通知,支付成功,一般跳转到成功页
        private  String return_url;
    
        // 签名方式
        private  String sign_type = "RSA2";
    
        // 字符编码格式
        private  String charset = "utf-8";
    
        // 支付宝网关; https://openapi.alipaydev.com/gateway.do
        private  String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";
    
        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 +"\","
                    + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
    
            String result = alipayClient.pageExecute(alipayRequest).getBody();
    
            //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
            System.out.println("支付宝的响应:"+result);
    
            return result;
    
        }
    }
    
    • 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

    PayVo:

    package com.henu.soft.merist.gulimall.order.vo;
    
    import lombok.Data;
    
    @Data
    public class PayVo {
        private String out_trade_no; // 商户订单号 必填
        private String subject; // 订单名称 必填
        private String total_amount;  // 付款金额 必填
        private String body; // 商品描述 可空
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    前端页面修改:

    image-20220819142848823

    支付宝的响应:<form name="punchout_form" method="post" action="https://openapi.alipaydev.com/gateway.do?charset=utf-8&method=alipay.trade.page.pay&sign=EocMxTOAZ6A0SpL3KNQ6FvrnKIDX5ueAXRgjKutK0TymEa3oJKVHs10if7d9MqJqv57%2FupGuvoM%2BLnrePofLAHs4rXYozU%2BI%2BlyKLF8ObsgIE9dD2xJ52Zqq7yERXlFSoT5G49x9%2Bp8Yn7XdOHoOweC%2FEhLn0aRVQWH7w%2B8Tnlp%2FTVlwckzL0A2RsHQaPY2njpYvzCOu252UMs%2B5Gg9o7h%2BAIKbOjvTir%2BVViXP3Ep%2FJWl36pGrjZMyQyK8SjHFSv%2FeD8q6k%2F6m2K3pxOu944hL3huH5MxNyheWd8qHVrsgTJNdIhzA%3D%3D&return_url=http%3A%2F%2Fmerist.free.idcfengye.com%2Falipay.trade.wap.pay-JAVA-UTF-8%2Freturn_url.jsp¬ify_url=http%3A%2F%2Fmerist.free.idcfengye.com%2Falipay.trade.wap.pay-JAVA-UTF-8%2Fnotify_url.jsp&version=1.0&app_id=2021000121650617&sign_type=RSA2×tamp=2022-08-19+16%3A35%3A54&alipay_sdk=alipay-sdk-java-dynamicVersionNo&format=json">
    <input type="hidden" name="biz_content" value="{"out_trade_no":"202208191635163351560545923401379842","total_amount":"12654.00","subject":"华为 HUAWEI Mate 30 Pro 翡冷翠 8GB+256GB麒麟990旗舰芯片OLED环幕屏双4000万徕卡电影四摄4G全网通手机","body":"","product_code":"FAST_INSTANT_TRADE_PAY"}">
    <input type="submit" value="立即支付" style="display:none" >
    </form>
    <script>document.forms[0].submit();</script>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接收请求的接口:

    根据上面支付宝响应的数据,我们需要传入响应的数据,这里使用了AlipayTemplate,传入响应的数据,自动访问alipay 的网关,进入支付页面

    package com.henu.soft.merist.gulimall.order.web;
    
    import com.alipay.api.AlipayApiException;
    import com.henu.soft.merist.gulimall.order.config.AlipayTemplate;
    import com.henu.soft.merist.gulimall.order.service.OrderService;
    import com.henu.soft.merist.gulimall.order.vo.PayVo;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    @Controller
    public class PayWebController {
        @Autowired
        AlipayTemplate alipayTemplate;
    
        @Autowired
        OrderService orderService;
    
        @ResponseBody
        @GetMapping(value = "/payOrder",produces = "text/html")
        public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException{
            PayVo payVo = orderService.getOrderPay(orderSn);
            String pay = alipayTemplate.pay(payVo);
            return pay;
        }
    }
    
    • 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
    @Override
    public PayVo getOrderPay(String orderSn) {
        PayVo payVo = new PayVo();
        OrderEntity order = this.getOrderByOrderSn(orderSn);
        BigDecimal bigDecimal = order.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
        payVo.setTotal_amount(bigDecimal.toString());
        payVo.setOut_trade_no(orderSn);
    
        //标题
        List<OrderItemEntity> order_sn = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
        OrderItemEntity itemEntity = order_sn.get(0);
        payVo.setSubject(itemEntity.getSkuName());
        //备注
        payVo.setBody(itemEntity.getSkuAttrsVals());
        return payVo;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    测试成功

    image-20220819164114298

    image-20220819164129080

    5.完善支付跳转页

    设置跳转到自己的支付完成跳转页

    image-20220819164317558

    测试:

    跳转成功

    image-20220819171213946

    6.订单页面完善

    在首页点击【我的订单】即可访问到订单页面,同时在member模块远程调用order 模块查询返回数据

    <li>
      <a href="http://member.gulimall.com/memberOrder.html">我的订单a>
    li>
    
    • 1
    • 2
    • 3
    @Controller
    public class MemberWebController {
    
        @Autowired
        OrderFeignService orderFeignService;
    
        @GetMapping("/memberOrder.html")
        public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model){
            //查出当前登录的用户的所有订单
            HashMap<String, Object> page = new HashMap<>();
            page.put("page",pageNum.toString());
            R r = orderFeignService.listWithItem(page);
            model.addAttribute("orders",r);
            return "orderList";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    远程服务调用丢失请求头数据,解决方法:

    @Configuration
    public class GuliFeignConfig {
        @Bean
        public RequestInterceptor requestInterceptor() {
            return new RequestInterceptor() {
                @Override
                public void apply(RequestTemplate template) {
                    //1. 使用RequestContextHolder拿到老请求的请求数据
                    ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                    if (requestAttributes != null) {
                        HttpServletRequest request = requestAttributes.getRequest();
                        if (request != null) {
                            //2. 将老请求得到cookie信息放到feign请求上
                            String cookie = request.getHeader("Cookie");
                            template.header("Cookie", cookie);
                        }
                    }
                }
            };
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    order 模块:

    @RestController
    @RequestMapping("order/order")
    public class OrderController {
        @Autowired
        private OrderService orderService;
    
        @PostMapping("/listWithItem")
        //@RequiresPermissions("order:order:list")
        public R listWithItem(@RequestBody Map<String, Object> params){
            PageUtils page = orderService.queryPageWithItem(params);
    
            return R.ok().put("page", page);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    /**
     * 订单支付完成跳转订单列表
     * 查询订单列表
     * @param params
     * @return
     */
    
    @Override
    public PageUtils queryPageWithItem(Map<String, Object> params) {
        MemberResponseTo memberResponseTo = LoginUserInterceptor.loginUser.get();
    
        IPage<OrderEntity> page = this.page(
                new Query<OrderEntity>().getPage(params),
                new QueryWrapper<OrderEntity>().eq("member_id",memberResponseTo.getId()).orderByDesc("id")
        );
    
        List<OrderEntity> order_sn = page.getRecords().stream().map(order -> {
            List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", order.getOrderSn()));
            order.setItemEntities(itemEntities);
            return order;
        }).collect(Collectors.toList());
    
        page.setRecords(order_sn);
        return new PageUtils(page);
    }
    
    • 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

    测试:

    image-20220819213205426

    7.支付宝支付异步回调修改订单状态

    • 支付宝支付完成之后,会跳转值指定的页面
    • 支付宝支付完成之后,会将支付的流水记录等信息以post的方式发送异步请求,现在需要发给订单服务,完成支付流水记录和订单状态修改
    • 收到回调信息后,直到回复success之后就不在发送

    异步通知路径

    image-20220819213357686

    修改内网穿透的本地地址为order.gulimall.com:80
    通过内存穿透的外网域名访问本地时,携带的Host头为外网的host头,从而导致无法访问

    解决方法:修改nginx,在nginx中修改外网的host头地址

    image-20220819214220976

    编写对应接口:

    /**
     * 异步接收支付宝成功回调
     */
    @RestController
    public class OrderPayedListener {
    
        @Autowired
        private AlipayTemplate alipayTemplate;
    
        @Autowired
        private OrderService orderService;
    
        @PostMapping("/payed/notify")
        public String handlerAlipay(HttpServletRequest request, PayAsyncVo payAsyncVo) throws AlipayApiException, AlipayApiException {
            System.out.println("收到支付宝异步通知******************");
            // 只要收到支付宝的异步通知,返回 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("支付宝异步通知验签成功");
                //修改订单状态
                orderService.handlerPayResult(payAsyncVo);
                return "success";
            }else {
                System.out.println("支付宝异步通知验签失败");
                return "error";
            }
        }
    
    }
    
    • 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
    @Override
    public void handlerPayResult(PayAsyncVo payAsyncVo) {
        //1.保存交易流水这个对象 PaymentInfoEntity
        PaymentInfoEntity paymentInfoEntity = new PaymentInfoEntity();
        paymentInfoEntity.setAlipayTradeNo(payAsyncVo.getTrade_no());
        paymentInfoEntity.setOrderSn(payAsyncVo.getOut_trade_no());//修改数据库为唯一属性
        paymentInfoEntity.setPaymentStatus(payAsyncVo.getTrade_status());
        paymentInfoEntity.setCallbackTime(payAsyncVo.getNotify_time());
        paymentInfoService.save(paymentInfoEntity);
    
        //2。修改订单状态
        if (payAsyncVo.getTrade_status().equals("TRADE_SUCCESS") || payAsyncVo.getTrade_status().equals("TRADE_FINISHED")) {
            //支付成功
            String outTradeNo = payAsyncVo.getOut_trade_no();
            this.baseMapper.updateOrderStatus(outTradeNo, OrderStatusEnum.PAYED.getCode());
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    image-20220819221554119

    image-20220819221615038

    8.收单(解决订单关闭后还能支付的问题)

    设置一个订单超时关闭订单的时间

    设置一分钟测试

    image-20220819221851626

    一分钟后:

    image-20220819222426090

  • 相关阅读:
    部署Django报错-requires SQLite 3.8.3 or higher
    【LeetCode】Day122-根据字符出现频率排序 & 最接近原点的 K 个点
    Spring系列19:SpEL详解
    NIO 笔记(二)Netty框架专题
    MySQL占用内存过大解决方案
    Qt5开发从入门到精通——第七篇三节( 图形视图—— 图元创建 GraphicsItem V1.0)
    【论文笔记】神经网络压缩调研
    新版本IntelliJ IDEA(如2023)中运行Spring Boot找不到VM options进行端口的修改的问题解决
    DW大学生网页作业制作设计 ——旅游门户网站(21页)HTML+CSS+JavaScript
    NOI2022 游记
  • 原文地址:https://blog.csdn.net/qq_52476654/article/details/126432823