SpringBoot微信点餐项目(二)——微信内调起支付宝支付

iwehdio的博客园:https://www.cnblogs.com/iwehdio/

代码:https://github.com/iwehdio/SpringBoot-WXDC

1、微信部分

  • 微信支付文档:【微信支付】JSAPI支付开发者文档

  • 微信公众账户测试号获取:公众号测试号

  • 使用natapp内网穿透:基于springboot接入微信公众号(内网穿透技术)。以下挨打url中,带有http://xxxx.natappfree.cc格式的均为请求到的不同的穿透域名。

  • 此外,还需要设置体验接口权限表下的网页账号的授权回调页面域名(不能填带/的路径)。

  • 获取openID:手工方式和SDK方式。

    • 请求:

      重定向到 /sell/wechat/authorize
      
    • 参数:

      returnUrl: http://xxx.com/abc  //【必填】
      
    • 返回:

      http://xxx.com/abc?openid=oZxSYw5ldcxv6H0EU67GgSXOUrVg
      
  • 网页回调手动获取openID:微信网页授权

    • 第一步:用户同意授权,获取code

      • 请求用户授权,访问:

        https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
        
        ----
        
        https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=http://hpqv53.natappfree.cc/sell/auth&response_type=code&scope=snsapi_base&state=123#wechat_redirect
        
      • 具体参数含义:

      参数 是否必须 说明
      appid 公众号的唯一标识
      redirect_uri 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理
      response_type 返回类型,请填写code
      scope 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 )
      state 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
      #wechat_redirect 无论直接打开还是做页面302重定向时候,必须带此参数
      • 如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。

        • code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
    • 第二步:通过code换取网页授权access_token

      • 获取code后,请求以下链接获取access_token:

        https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
        
        --- 改成具体的APPID和SECRETID
        
        https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRETID&code=CODE&grant_type=authorization_code
        
      • 参数说明:

        参数 是否必须 说明
        appid 公众号的唯一标识
        secret 公众号的appsecret
        code 填写第一步获取的code参数
        grant_type 填写为authorization_code
      • 返回格式:

        //成功时
        {
          "access_token":"ACCESS_TOKEN",
          "expires_in":7200,
          "refresh_token":"REFRESH_TOKEN",
          "openid":"OPENID",
          "scope":"SCOPE" 
        }
        
        //错误时
        {"errcode":40029,"errmsg":"invalid code"}
        
      • 参数说明:

        参数 描述
        access_token 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
        expires_in access_token接口调用凭证超时时间,单位(秒)
        refresh_token 用户刷新access_token
        openid 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID
        scope 用户授权的作用域,使用逗号(,)分隔
    • 代码:

      @RestController
      public class WeiXinController {
          private final Logger logger = LoggerFactory.getLogger(WeChatController.class);
          @GetMapping("/auth")
          public void auth(@RequestParam("code") String code) {
              logger.info("code={}",code);
              String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRETID&code="+code+"&grant_type=authorization_code";
              RestTemplate restTemplate = new RestTemplate();
              String access = restTemplate.getForObject(url, String.class);
              logger.info(access);
          }
      }
      
  • 使用SDK方式:MP_OAuth2网页授权

    • maven依赖:

      <dependency>
          <groupId>com.github.binarywang</groupId>
          <artifactId>weixin-java-mp</artifactId>
          <version>2.7.0</version>
      </dependency>
      
      <!-- Springboot的配置类 -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-configuration-processor</artifactId>
          <optional>true</optional>
      </dependency>
      
    • 配置类:

      @Component
      @ConfigurationProperties(prefix = "wechat")
      public class WechatAccountConfig {
          private String mpAppId;
          private String mpAppSecret;
      }
      
    • 配置文件:

      wechat:
        mpAppId: wx4451dcc6f165f0f4
        mpAppSecret: cf93b89968d2fdf90af56d733ccd1a16
      
    • 自动配置:

      @Component
      @Configuration
      public class WechatMConfig {
          @Autowired
          private WechatAccountConfig accountConfig;
          @Bean
          public WxMpService wxMPService(){
              WxMpService wxMpService = new WxMpServiceImpl();
              wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
              return wxMpService;
          }
          @Bean
          public WxMpConfigStorage wxMpConfigStorage(){
              WxMpInMemoryConfigStorage wxMpConfigStorage = new WxMpInMemoryConfigStorage();
              wxMpConfigStorage.setAppId(accountConfig.getMpAppId());
              wxMpConfigStorage.setSecret(accountConfig.getMpAppSecret());
              return wxMpConfigStorage;
          }
      }
      
    • SDK网页授权:

      1. authorize方法相当于获取code。
      2. userInfo方法相当于根据code获取openid。
      @Controller
      @RequestMapping("/wechat")
      public class WechatController {
          @Autowired
          private WxMpService wxMpService;
          @GetMapping("/authorize")
          public String authorize(@RequestParam("returnUrl") String returnUrl){
              String url = "http://hpqv53.natappfree.cc/sell/wechat/userInfo";
              String redirectUrl = wxMpService.oauth2buildAuthorizationUrl(url, WxConsts.OAUTH2_SCOPE_BASE, returnUrl);
              System.out.println(redirectUrl);
              return "redirect:"+redirectUrl;
      
          }
          @GetMapping("/userInfo")
          public String userInfo(@RequestParam("code") String code,
                               @RequestParam("state") String returnUrl){
              WxMpOAuth2AccessToken accessToken;
              try {
                  accessToken = wxMpService.oauth2getAccessToken(code);
              } catch (WxErrorException e) {
                  throw new SellException(ResultEnum.WECHAT_MP_ERROR,e.getError().getErrorMsg());
              }
              String openId = accessToken.getOpenId();
              return "redirect:"+returnUrl+"?openid="+openId;
          }
      }
      
    • 测试请求以下链接:

      http://hpqv53.natappfree.cc/sell/wechat/authorize?returnUrl=http://www.imooc.com
      
  • 接入前端调试:

    • 首先更改前端项目中的/config/index.js中:

      sellUrl: 'sell.com',
      openidUrl: 'http://43bwcy.natappfree.cc/sell/wechat/authorize',
      
    • 然后npm run build编译,编译后的文件在dist目录下,再复制到Nginx部署静态资源的位置。

    • 获取电脑和手机的IP地址。测试能不能ping通。

    • 在手机上设置手动HTTP代理,地址为电脑的IP地址,端口为8888。

    • 下载fiddler,设置监听端口为8888,并且允许远程连接。

2、支付部分

  • 主要用到的是统一下单和支付结果通知两个API。

  • 使用SDK:支付SDK

    • 请求:

      重定向 /sell/pay/create
      
    • 参数:

      orderId:16465165415452
      returnUrl:http://xxx.com/abc/order/16465165415452
      
    • 返回:

      http://xxx.com/abc/order/16465165415452
      
    • maven依赖:

      <dependency>
          <groupId>cn.springboot</groupId>
          <artifactId>best-pay-sdk</artifactId>
          <version>1.1.0</version>
      </dependency>
      
  • 但是微信测试号不提供支付功能,只能用在微信内拉起支付宝的方式测试,因为支付宝提供了沙箱测试环境。开放平台-沙箱环境 (alipay.com)

  • 使用官方的老SDK

    • 引入老SDK的maven依赖:

      <dependency>
          <groupId>com.alipay.sdk</groupId>
          <artifactId>alipay-sdk-java</artifactId>
          <version>4.10.167.ALL</version>
      </dependency>
      
    • 根据官方的接入文档,主要是对AlipayClient和AlipayTradeWapPayRequest进行一些配置,然后用form = alipayClient.pageExecute(alipayRequest).getBody()获取提交到前端的表单,用response.getWriter().write(form)输出。

  • 支付宝支付相关的配置类:

    • 配置类:

      @Component
      @ConfigurationProperties(prefix = "alipay")
      public class AlipayConfig {
          String charset = "UTF-8";
          String appId;
          String appPrivateKey;
          String alipayPublicKey;
          String serverUrl = "https://openapi.alipaydev.com/gateway.do";
          String baseUrl;
      }
      
    • 配置文件:

      alipay:
        app-id: 
        alipay-public-key: 
        app-private-key: 
        base-url: 
      
    • 可以先用这种方式测试一下。

  • 支付宝针对官方的老SDK提供了实现的Demo

    • demo中,ap.js提供了跳转逻辑,pay.htm是提供了跳转页面,另外两个是示例。使用需要注意的是要在htm文件中正确配置ap.js的路径。
    • 以get方式为例,最终访问的是demo_get.htm下的确认支付的a标签中的href路径。如果使用get方式,需要将后端的请求数据动态传入,可以使用freemaker模板引擎。
  • freemaker模板引擎:

    • maven依赖:

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-freemarker</artifactId>
      </dependency>
      
    • 前端使用:

      • 在template目录下创建后缀为.ftl的文件。
      • 使用${键名}的方式获取后端传入的数据,对于对象使用${键名.属性名}
    • 后端数据:

      • 返回一个ModelAndView对象,设置viewName为.ftl文件名。
      • 设置model为一个map形式的数据。
  • 实现微信调用支付宝支付:

    • 主要就是要传给前端一个拼接好的url。

    • alipayClient.pageExecute(alipayRequest).getBody()返回的String对象是一个form表单,写到前端是用post方式。

      • 而没有找到单独的获取url等的方法,只好用截取form字符串的方法。(其实用post的demo可能好一点?)
      • 在form表单中,只有biz_content属性是在表单属性,其他都被拼接在了action属性中。
    • 必须调用alipayClient.pageExecute(alipayRequest)方法的原因是,需要根据支付宝公钥和应用私钥获得签名sign属性,当然也可以自行实现。而且经过这个方法后,其中的属性都被UrlEncode了(除了biz_content)。

    • 所以解决办法是对form字符串进行截取(其实不是一个很好的方法),然后拼接UrlEncode后的biz_content属性。

    • 测试控制器:

      @Controller
      @RequestMapping("/pay")
      public class PayController {
          @Autowired
          private AlipayConfig alipayConfig;
          @GetMapping("/create")
          public ModelAndView create(@RequestParam("orderId") String orderId,
                             @RequestParam("returnUrl") String returnUrl,
                             HttpServletResponse response){
      
              String CHARSET = alipayConfig.getCharset();
              String APP_ID = alipayConfig.getAppId();
              String APP_PRIVATE_KEY = alipayConfig.getAppPrivateKey();
              String ALIPAY_PUBLIC_KEY = alipayConfig.getAlipayPublicKey();
              AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig.getServerUrl(), APP_ID, APP_PRIVATE_KEY, "json", CHARSET, ALIPAY_PUBLIC_KEY, "RSA2"); //获得初始化的AlipayClient
              AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
              String baseUrl = alipayConfig.getBaseUrl();
              alipayRequest.setReturnUrl(baseUrl+"/pay/returnUrl");
              alipayRequest.setNotifyUrl(baseUrl+"/pay/notifyUrl");//在公共参数中设置回跳和通知地址
              alipayRequest.setBizContent("{" +
                      " "out_trade_no":"201603200756150101002"," +
                      " "total_amount":"88.88"," +
                      " "subject":"Iphone6 84G"," +
                      " "product_code":"QUICK_WAP_PAY"" +
                      " }");
      
              String form = "";
              try {
                  form = alipayClient.pageExecute(alipayRequest).getBody();
              } catch (AlipayApiException e) {
                  e.printStackTrace();
              }
              String oriUrl = StringUtils.substringBetween(form,alipayConfig.getServerUrl(),"">");
              String biz_content = "";
              try {
                  biz_content = URLEncoder.encode(alipayRequest.getBizContent(),"utf-8");
              } catch (UnsupportedEncodingException e) {
                  e.printStackTrace();
              }
              String toUrl = oriUrl + "&biz_content=" + biz_content;
      
              toUrl = alipayConfig.getServerUrl() + toUrl;
              Map<String,String> map = new HashMap<>();
              map.put("toUrl",toUrl);
              return new ModelAndView("confirm_order",map);
          }
      }
      
    • 前端模板页面confirm_order.ftl,其实就是把demo_get.htm中a标签中具体的url变成了${toUrl}

  • 实现效果:

  • 实现具体业务:

    • 根据传入的orderId查询订单详情,获取相关信息并且传到前端。

    • 控制器:

      @Controller
      @RequestMapping("/pay")
      public class PayController {
          private Logger logger = LoggerFactory.getLogger(PayController.class);
          @Autowired
          private OrderMasterService masterService;
          @Autowired
          private PayService payService;
          @GetMapping("/create")
          public ModelAndView create(@RequestParam("orderId") String orderId,
                                     @RequestParam("returnUrl") String returnUrl) {
              OrderDTO orderDTO = masterService.findOne(orderId);
              if (orderDTO == null) {
                  logger.error("【支付出错】订单不存在,orderId={}",orderId);
                  throw new SellException(ResultEnum.ORDER_NOT_EXIST);
              }
              //拼接订单中的商品名
              StringBuilder subject = new StringBuilder();
              List<OrderDetail> orderDetailList = orderDTO.getOrderDetailList();
              for (OrderDetail orderDetail : orderDetailList) {
                  subject.append(orderDetail.getProductName()).append(" ");
              }
              Map<String, String> returnMap = payService.create(orderDTO);
              Map<String, String> map = new HashMap<>();
              map.put("toUrl", returnMap.get("toUrl"));
              try {
                  map.put("checkUrl", returnMap.get("checkUrl") + "&returnUrl=" + URLEncoder.encode(returnUrl,"utf-8"));
              } catch (UnsupportedEncodingException e) {
                  e.printStackTrace();
              }
              map.put("productName",subject.toString());
              map.put("amount", orderDTO.getOrderAmount().toString());
              map.put("buyerName",orderDTO.getBuyerName());
              map.put("orderTime", orderDTO.getCreateTime().toString());
              map.put("orderId", orderId);
              return new ModelAndView("confirm_order", map);
          }
      }
      
    • 业务层:

      @Service
      public class PayServiceImpl implements PayService {
          @Autowired
          private AlipayConfig alipayConfig;
          @Autowired
          private OrderMasterService masterService;
          private Logger logger = LoggerFactory.getLogger(PayServiceImpl.class);
          @Override
          public Map<String, String> create(OrderDTO orderDTO) {
              String subject = getProductNames(orderDTO);
              String CHARSET = alipayConfig.getCharset();
              String APP_ID = alipayConfig.getAppId();
              String APP_PRIVATE_KEY = alipayConfig.getAppPrivateKey();
              String ALIPAY_PUBLIC_KEY = alipayConfig.getAlipayPublicKey();
              AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig.getServerUrl(), APP_ID, APP_PRIVATE_KEY, "json", CHARSET, ALIPAY_PUBLIC_KEY, "RSA2"); //获得初始化的AlipayClient
              AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
              String baseUrl = alipayConfig.getBaseUrl();
              alipayRequest.setReturnUrl(baseUrl + "/pay/backwechat");
              alipayRequest.setNotifyUrl(baseUrl + "/pay/notifyUrl");//在公共参数中设置回跳和通知地址
              alipayRequest.setBizContent("{" +
                      " "out_trade_no":"" + orderDTO.getOrderId() + ""," +
                      " "total_amount":"" + orderDTO.getOrderAmount() + ""," +
                      " "subject":"" + subject + ""," +
                      " "product_code":"QUICK_WAP_PAY"" +
                      " }");
              String form = "";
              try {
                  form = alipayClient.pageExecute(alipayRequest).getBody();
              } catch (AlipayApiException e) {
                  e.printStackTrace();
              }
              String oriUrl = StringUtils.substringBetween(form, alipayConfig.getServerUrl(), "">");
              String biz_content = "";
              try {
                  biz_content = URLEncoder.encode(alipayRequest.getBizContent(), "utf-8");
              } catch (UnsupportedEncodingException e) {
                  e.printStackTrace();
              }
              String toUrl = oriUrl + "&biz_content=" + biz_content;
      
              toUrl = alipayConfig.getServerUrl() + toUrl;
              String checkUrl = baseUrl + "/pay/checkpay?orderId=" + orderDTO.getOrderId();
              Map<String, String> map = new HashMap<>();
              map.put("toUrl",toUrl);
              map.put("checkUrl",checkUrl);
      
              return map;
          }
      
          private String getProductNames(OrderDTO orderDTO) {
              StringBuilder subject = new StringBuilder();
              List<OrderDetail> orderDetailList = orderDTO.getOrderDetailList();
              for (OrderDetail orderDetail : orderDetailList) {
                  subject.append(orderDetail.getProductName()).append(" ");
            }
              return subject.toString();
          }
      }
      
  • 异步通知:

    • 验证签名。

    • 获取返回的支付状态。

    • 校验支付金额。

    • 全部校验通过后,通知支付平台停止异步通知。

    • 具体见支付宝异步回调文档

    • 控制器:

      @PostMapping("/notifyUrl")
      public void notifyUrl(HttpServletRequest request, HttpServletResponse response) {
          boolean signVerified = payService.notifyUrl(request, response);
          if (signVerified) {
              masterService.paid(masterService.findOne(request.getParameter("out_trade_no")));
          }
          logger.info("【异步回调】回调结果{},orderId={}",signVerified,request.getParameter("out_trade_no"));
      }
      
    • 业务层:

      @Override
      public boolean notifyUrl(HttpServletRequest request, HttpServletResponse response) {
          response.setContentType("text/html;charset=utf-8");
      
          Map<String, String> paramsMap = new HashMap<>(); //将异步通知中收到的所有参数都存放到map中
          Map<String, String[]> requestParams = request.getParameterMap();
          for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
              String name = iter.next();
              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] + ",";
              }
              paramsMap.put(name, valueStr);
          }
      
          boolean signVerified = false; //调用SDK验证签名
          try {
              signVerified = AlipaySignature.rsaCheckV1(paramsMap, alipayConfig.getAlipayPublicKey(), alipayConfig.getCharset(), alipayConfig.getSign_type());
          } catch (AlipayApiException e) {
              e.printStackTrace();
          }
      
      
          boolean check = checkBotifyRequest(paramsMap);
          try {
              PrintWriter writer = response.getWriter();
              check = checkBotifyRequest(paramsMap);
              if(signVerified && check){
                  writer.write("success");
                  logger.info("【回调支付成功】,orderId={}",request.getParameter("out_trade_no"));
              }else{
                  writer.write("failure");
                  logger.info("【回调支付失败】,orderId={}",request.getParameter("out_trade_no"));
              }
              response.getWriter().close();
          } catch (IOException e) {
              e.printStackTrace();
          }
      
          return signVerified && check;
      }
      
  • 还要考虑的一点是从外部浏览器支付完成后如何跳转回微信:

    • 首先,直接用同步回调returnUrl(固定为/backwechat)回到微信:

      • 控制器:

        @GetMapping("/backwechat")
        public ModelAndView backwechat(){
            return new ModelAndView("jumpback");
        }
        
      • 前端:

        <script>
            window.onload =function () {
                window.location = "weixin://"
            }
        </script>
        
      • 这样回到的就是跳转之前的微信界面,但是现在的问题是如何进入下一步。

    • 改造原有的跳转页面,增加一个支付完成的按钮。

      • 添加在pay.htm中:

        <div class="wrapper buy-wrapper">
            <a id="checkpay" class="J-btn-submit btn mj-submit btn-strong btn-larger btn-block">支付完成</a>
        </div>
        
        var checkUrl = getQueryString(location.href, "checkUrl");
        var returnUrl = getQueryString(location.href, "returnUrl");
        document.querySelector("#checkpay").href = checkUrl + "&returnUrl=" + returnUrl;
        
      • 为了能获取checkUrl,需要对之前的前端文件进行一些改造:

        • confir_order.ftl:

          <input id="orderId" type="hidden" value=${checkUrl} />
          
          var checkUrl = document.getElementById("orderId").value;
          
          _AP.pay(e.target.href, checkUrl);
          
        • ap.js:

          b.pay = function(d, checkUrl) {
              var c = encodeURIComponent(a.encode(d));
              location.href = "pay.htm?goto=" + c + "&checkUrl=" + checkUrl;
          };
          
      • 后端的处理:

        • PayServiceImpl中拼接checkUrl,同时使用一个map返回toUrl和checkUrl:

          String checkUrl = baseUrl + "/pay/checkpay?orderId=" + orderDTO.getOrderId();
          
        • PayController中放入map中:

          map.put("checkUrl", returnMap.get("checkUrl"));
          
    • 最后的结果:

      • 如果支付前直接点支付完成,会查询订单是否完成。
      • 支付完成后在浏览器中点击完成付款,会跳转到微信中,然后点击支付完成会跳转到最终页面。

  • 接入前端调试:

    • 首先更改前端项目中的/config/index.js中:

      sellUrl: 'http://sell.com',
      openidUrl: 'http://5w3ab3.natappfree.cc/sell/wechat/authorize',
      wechatPayUrl: 'http://5w3ab3.natappfree.cc/sell/pay/create'
      
    • 重新部署前端。

  • 退款:

    • 使用请求AlipayTradeRefundRequest,传入orderId和退款金额。

    • 业务层:

      @Override
      public void refund(String orderId) {
          String CHARSET = alipayConfig.getCharset();
          String APP_ID = alipayConfig.getAppId();
          String APP_PRIVATE_KEY = alipayConfig.getAppPrivateKey();
          String ALIPAY_PUBLIC_KEY = alipayConfig.getAlipayPublicKey();
          AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig.getServerUrl(), APP_ID, APP_PRIVATE_KEY, "json", CHARSET, ALIPAY_PUBLIC_KEY, "RSA2"); //获得初始化的AlipayClient
          AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();//创建API对应的request类
          OrderDTO orderDTO = masterService.findOne(orderId);
          request.setBizContent("{" +
                                ""out_trade_no":"" + orderId + ""," +
                                ""out_request_no":"1000001"," +
                                ""refund_amount":"0" + orderDTO.getOrderAmount() + ""}"); //设置业务参数
          AlipayTradeRefundResponse response = null;//通过alipayClient调用API,获得对应的response类
          try {
              response = alipayClient.execute(request);
          } catch (AlipayApiException e) {
              e.printStackTrace();
          }
          System.out.print(response.getBody());
      }
      
    • 在orderMasterService的cancel取消订单方法中最后被调用。


iwehdio的博客园:https://www.cnblogs.com/iwehdio/
原文地址:https://www.cnblogs.com/iwehdio/p/14111428.html