微信公众号开发--支付完整流程

微信公众号支付的完整流程,首先需要微信授权,获取openId,因为openid是微信用户在公众号appid下的唯一用户标识(appid不同,则获取到的openid就不同),可用于永久标记一个用户,同时也是微信JSAPI支付的必传参数。

首先解释一下微信公众号中的一些概念,想要完成支付,需要被认证的公众号外,还需要商户号。这两个都需要有一定资质才能申请。单纯拥有公众号,只能进行微信授权操作,需要公众号和商户号绑定后才能完成支付操作。
用个不够恰当的例子来解释:公众号就类比于银行的前台,商户号就类比于银行,前台不绑定银行的话那她就是一个普通人,不能完成银行的各类经济业务,前台和银行绑定后才可以操作款项。而这个前台也要通过有一定的资质认证,才能和银行绑定。
注:微信支付的接口不只有公众号支付一种,但是无论哪种支付接口,都需要绑定商户号才能进行支付操作。

获取OpenId

获取OpenId,有两种方式,“手工方式”和“利用第三方API”,最终目的都是一样的,但是在实际开发中还是用轮子比较容易。手工方式最主要的是一步一步的了解获取OpenId的过程,如果以使用为主,可以直接跳过“手工方式”,查看“利用第三方API”。


手工方式

首先,很重要也是很多人懒得去做的事情就是仔细看看【微信支付】商户接入文档,内容很多,因为是微信公众号,所以我选择JSAPI支付

微信公众号开发--支付完整流程_第1张图片
普通商户接入文档界面

打开链接可以看到JSAPI支付的详细内容,接下来的操作都是根据JSAPI支付中的“业务流程”逐步完成。
微信公众号开发--支付完整流程_第2张图片
JSAPI支付详细

PS.其实下图链接 网页授权获取用户openid接口文档也是真的写的很清楚了。当trade_type=JSAPI时(即公众号支付),openId必传。
微信公众号开发--支付完整流程_第3张图片
openId的重要性

1. 设置网页授权域名

按照文档来,在公众号中【设置网页授权域名】,这里接入的是外网地址,通俗但不够准确的讲,就是你程序所在的域名。

微信公众号开发--支付完整流程_第4张图片

填写完域名之后,记得下载文件,因为规定很多,还需要ICP备案什么的,作为调试,我选择使用了 https://natapp.cn/穿透内网,就可以使得微信这边访问到自己的电脑。
微信公众号开发--支付完整流程_第5张图片

在NATAPP中,注册/登录,购买隧道,免费的只能临时用一下,还会随便换域名/端口,所以我购买了9/月的。
微信公众号开发--支付完整流程_第6张图片

查看购买到的隧道,这个域名是你在购买过程中,他让你自己输入的。
微信公众号开发--支付完整流程_第7张图片

查看教程NATAPP 1分钟快速图文教程,启动NATAPP。启动后会看到域名映射到当前本地端口。
此时通过在浏览器中,输入localhost:8080/myselfhttp://my.natapp.com/myself 访问的是同一个界面则表示成功。
再将之前在微信【网页授权域名】中下载的 MP_verify_nxxxxxx.txt文件放到源码文件中。
注意:这里要求文件的位置,必须是在域名的根目录下,在本例中也就是在浏览器中输入http://my.natapp.com/MP_verify_nxxxxxx.txt后,页面不报错时,在点击【确认】才能在微信网页授权域名中添加成功。
当然,添加成功域名之后,这个MP_verify_nxxxxxx.txt文件也可以从源码文件中删除。

2. 获取code

可以查看微信文档,并了解相应参数说明。以下是微信文档的相应截图,我们只需要了解的就是替换掉链接中的appId和redirect_uri。
微信公众号开发--支付完整流程_第8张图片
微信公众号开发--支付完整流程_第9张图片
参数说明

用户同意授权后,跳转到redirect_uri并返回code。
微信公众号开发--支付完整流程_第10张图片
用户同意授权
3. 换取access_token

获取了code之后,以code作为票据再换access_token。以下是微信文档的相应截图,我们只需要了解的就是替换掉链接中的appId为自己的appId和code为刚刚后台获取的code(注:code时效只有5min)。
微信公众号开发--支付完整流程_第11张图片
换取access_token
4. 得到openId

在上一个步骤中,获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。请求上述链接时,正确时会返回如下JSON数据包。其中就包含了openId。
微信公众号开发--支付完整流程_第12张图片
JSON数据包

更多使用,可以仔细查看微信文档!!文档写的很细致的!!

以我自己为例,当我手机微信访问了下面这个地址(网页授权URL)之后
https://open.weixin.qq.com/connect/oauth2/authorize?appid=myappId&redirect_uri=http://my.natapp.com/myRedirectURL&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
手机微信会自动跳转到http://my.natapp.com/myRedirectURL/code=xxcodexx,虽然手机页面是空白,但是后台已经获得了code的信息。后台可以通过拼接字符串等操作,再发起请求https://api.weixin.qq.com/sns/oauth2/access_token?appid=myappId&secret=SECRET&code=xxcodexx&grant_type=authorization_code,之后获得一个包含了openId信息的JSON数据包。


利用第三方API

直接看Github上的SDK:https://github.com/Wechat-Group/WxJava。里面文档、工具都非常详细。
因为我本地程序用的Maven,所以直接引用。[pom代码1]


  com.github.binarywang
  weixin-java-mp
  3.5.0

在本地函数中的使用,主要查看文档https://github.com/Wechat-Group/WxJava/wiki/MP_OAuth2网页授权
根据文档逐步完成本地代码。
新建WechatController.class,控制网络授权。[授权代码1]

@Controller
@RequestMapping("/wechat")
@Slf4j
public class WechatController {

    @Autowired
    WxMpService wxMpService = new WxMpServiceImpl();

    @GetMapping("/authorize")
    public String authorize(@RequestParam("returnUrl") String returnUrl) {
        // 1.配置,项目中配置应该是进行一个统一配置,供程序各个部分使用。
        // 2.调用方法,下面这个回调地址 是我自己的地址,你需要用你自己的
        String url = "http://sell35.natapp1.cc/sell/wechat/userInfo";
        String redirectUrl = wxMpService.oauth2buildAuthorizationUrl(url, WxConsts.OAUTH2_SCOPE_BASE, URLEncoder.encode(returnUrl));
        log.info("【微信网页授权】获取code, result={}", redirectUrl);

        return "redirect:" + redirectUrl;
    }

    // 获取用户信息 
    @GetMapping("/userInfo")
    public String userInfo(@RequestParam("code") String code,
                         @RequestParam("state") String returnUrl) {
        WxMpOAuth2AccessToken wxMpOAuth2AccessToken = new WxMpOAuth2AccessToken();
        try {
            wxMpOAuth2AccessToken = wxMpService.oauth2getAccessToken(code);
        } catch (WxErrorException e) {
            log.error("【微信网页授权】{}", e);
            throw new SellException(ResultEnum.WECHAT_MP_ERROR.getCode(), e.getError().getErrorMsg());
        }
        String openId = wxMpOAuth2AccessToken.getOpenId();

        return "redirect:" + returnUrl + "?openid=" + openId;

    }
}

建立config文件夹,并在下面新建WeChatMpConfig.class。将Service作为一个Bean、配置也作为Bean。 其中的AppId和AppSecret我们可以从配置文件中读取。[授权代码2]

@Component
public class WechatMpConfig {

    @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;
    }
}

配置文件
微信公众号开发--支付完整流程_第13张图片

微信账号相关的部分先写一个配置文件。WechatAccountConfig.class[授权代码3]

@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatAccountConfig {

    private String mpAppId;

    private String mpAppSecret;
}


前端调试

首先先讲一下请求过程,微信访问sell.com,前端回会重定向到 /sell/wechat/authorize,并携带returnUrl:http://sell.com/abc。 通过上一步骤的授权操作获取openid,最后后端返回给前端 :http://sell.com/abc?openid=oxfjhaojdnsjcos

所以需要先在前端重定向,进入虚拟机(前端部署部分)
cd /opt/
cd code/
cd sell_fe_buyer/
cd config/
配置文件
vim index.js
在配置文件中
sellUrl对应的是项目地址:http://sell.com
openidUrl获取openId的地址:http://sell35.natapp.cc/sell/wechat/authorize
wechatPayUrl支付地址(当前主要是配置授权,先无需配置这一项)

配置完成后,回到前端项目的根目录(cd ..)。

再构建一下(npm run build)

构建好的文件,在dist目录下,所以需要将构建好的文件copy到前端的根目录下,语句如下。
cp -r dist/* /opt/data/wwwroot/sell

但是此时我们通过手机访问"sell.com"是访问不了的。这是因为当前目标网址“sell.com”是在电脑端,电脑之所以能访问,是因为本机设置了host,他将域名直接指向虚拟机地址,所以可以成功访问,但是由于手机无法更改host。所以需要用代理解决这个问题。将手机的所有请求转发到电脑上,此时就可以访问了。Mac下可以使用Charles,Windows下可以使用fiddler,

通过终端输入ifconfig,得到当前电脑ip为 192.168.1.103.
再通过手机查询当前手机的ip为 192.168.1.105.

最好二者接通前在terminal中ping一下。

ping通之后,在手机中设置手动代理
服务器中输入:电脑ip(103)
端口输入:8888(因为Charles的默认端口就8888)

此时再在手机端访问sell.com,就可以通过电脑访问到公众号网站。


微信支付

支付业务流程:生成商户订单(开发者生成的订单) —> 调用统一下单API —> 生成预付单后会返回一个预付单信息 —> 通过JSAPI页面调用的支付参数并签名(此时才会唤起支付) —> 支付完成后等待一个异步通知结果 —> 依据这个结果通知更改订单状态为已支付 —> 调用查询API,查询支付结果(用于对账)

选择SDK,可以选择之前的那个SDK,这里我选择的是Best Pay SDK。

请求过程:

  1. 重定向到 /sell/pay/create,携带参数(orderId:123456returnUrl:http://xxx.com/abc/order/123456
  2. 最后返回到 http://xxx.com/abc/order/123456

这里需要注意的是,支付过程中,只需要传过来订单ID即可,至于需要支付多少钱,可以通过订单ID去数据库查看。不能将支付金额作为参数往后传递,因为这样即便金额不对,也能够支付成功,或者后台再校验一边金额,无论怎样,都是多此一举。

引入SDK,在Pom文件中添加依赖。[pom代码2]


  cn.springboot
  best-pay-sdk
  1.1.0

新建一个PayController Class,主要完成订单查询和支付操作。[该段代码非最终代码,至于此处以便与思考] 。

@Controller
@RequestMapping("/pay")
public class PayController {

    @Autowired
    private OrderService orderService;

    // PayService之后作为服务新建,当前并不存在。
    @Autowired
    private PayService payService;

    // 为了重定向,完成请求过程的第一步
    @GetMapping("/create")
    public void create(@RequestParam("orderId") String orderId,
                       @RequestParam("returnUrl") String returnUrl) {
        //1. 查询订单
        OrderDTO orderDTO = orderService.findOne(orderId);
        if (orderDTO == null) {
            throw new SellException(ResultEnum.PRODUCT_NOT_EXIST);
        }

        //2. 发起支付
        PayResponse payResponse = payService.create(orderDTO);
    }
}

根据SDK规则,微信账户相关内容需要配置,配置在WechatAccountConfig中。[代码3]

@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatAccountConfig {

    private String mpAppId;

    private String mpAppSecret;

    //    商户号
    private String mchId;
    //    商户密钥
    private String mchKey;
    //    商户证书路径
    private String keyPath;
    //    微信支付异步通知地址
    private String notifyUrl;
}

同时修改给配置文件增加相应内容
微信公众号开发--支付完整流程_第14张图片

配置一下WechatPayConfig(),并把service作为Bean配置进去。[代码4]

@Component
public class WechatPayConfig {
    @Autowired
    private WechatAccountConfig accountConfig;

    @Bean
    public BestPayServiceImpl bestPayService(){
        WxPayH5Config wxPayH5Config=new WxPayH5Config();
        wxPayH5Config.setAppId(accountConfig.getMpAppId());
        wxPayH5Config.setAppSecret(accountConfig.getMpAppSecret());
        wxPayH5Config.setMchId(accountConfig.getMchId());
        wxPayH5Config.setMchKey(accountConfig.getMchKey());
        wxPayH5Config.setKeyPath(accountConfig.getKeyPath());
        wxPayH5Config.setNotifyUrl(accountConfig.getNotifyUrl());

        BestPayServiceImpl bestPayService=new BestPayServiceImpl();
        bestPayService.setWxPayH5Config(wxPayH5Config);

        return bestPayService;
    }

支付操作作为一个服务,新建PayService,并建立该方法的实现PayServiceImpl。[代码6] 并且将之前BestPayServiceImpl配置好的注入进Service。上述的JsonUtil是个JSON格式化工具类,已附追在文章末尾。

@Service
@Slf4j
public class PayServiceImpl implements PayService {
    private static final String ORDER_NAME = "微信点单订餐";

    @Autowired
    private BestPayServiceImpl bestPayService;

    @Override
    public PayResponse create(OrderDTO orderDTO) {
        PayRequest payRequest = new PayRequest();
        payRequest.setOpenid(orderDTO.getBuyerOpenid());
        payRequest.setOrderAmount(orderDTO.getOrderAmount().doubleValue());
        payRequest.setOrderId(orderDTO.getOrderId());
        payRequest.setOrderName(ORDER_NAME);
        payRequest.setPayTypeEnum(BestPayTypeEnum.WXPAY_H5);
        log.info("【微信支付】,发起支付,request={}", JsonUtil.toJson(payRequest));

        PayResponse payResponse = bestPayService.pay(payRequest);
        log.info("【微信支付】,发起支付,response={}", JsonUtil.toJson(payResponse));
        return payResponse;
    }
}

最后返回的 response 内容包含了 "appId""timeStamp""nonceStr""packAge""signType""paySign"的值。
此时完成了业务流程中的:调用统一下单API,并且返回预付单信息prepay_id"packAge"对应的值中)。
下一步我们要做的就是发起支付


从网页发起支付

支付操作的详细内容先仔细阅读文档JSAPI支付开发者文档
需要向后端先传递这些参数。

微信公众号开发--支付完整流程_第15张图片

之后会返回如下图所示的前端代码,这个代码就是最后生成微信支付页的部分。
微信公众号开发--支付完整流程_第16张图片

所以在代码部分,我们接下来的工作就是动态构造如上图所示的代码。

这里我们选择模版技术,用到了freemarker这个组件,现在pom文件中引入dependency


     org.springframework.boot
     spring-boot-starter-freemarker

完善之前的PayController Class[代码5] 。将返回的参数从void改成ModelAndView,最后return返回的“pay/create”路径下的create实际上就是一个create.flt文件(模版文件)。

@Controller
@RequestMapping("/pay")
public class PayController {

    @Autowired
    private OrderService orderService;

    @Autowired
    private PayService payService;

    // 为了重定向,完成请求过程的第一步
    @GetMapping("/create")
    public ModelAndView create(@RequestParam("orderId") String orderId,
                               @RequestParam("returnUrl") String returnUrl,
                               Map map) {
        //1. 查询订单
        OrderDTO orderDTO = orderService.findOne(orderId);
        if (orderDTO == null) {
            throw new SellException(ResultEnum.PRODUCT_NOT_EXIST);
        }

        //2. 发起支付
        PayResponse payResponse = payService.create(orderDTO);
        map.put("payResponse", payResponse);
        map.put("returnUrl", returnUrl);

        return new ModelAndView("pay/create");

    }
}

create.flt中放的就是微信内H5调起支付
文档中返回的代码格式


此时已经完成动态注入参数了,但是完成支付还需要我们在前端文件中配置一下,参考之前的【前端调试】模块。
微信公众号开发--支付完整流程_第17张图片

记得改完之后,build和拷贝文件。

此时再去支付,支付完成后发现并没有得到“支付成功”的通知,这是因为我们没有修改订单状。在微信的支付业务流程中,我们还没有做处理微信异步通知结果这一步。
所以我们下一步的工作就是:接受微信的异步通知结果,并根据结果更改订单的支付状态。


微信异步通知

在微信内H5调起支付时,前端也可以接收到一个是否成功的标志。
微信公众号开发--支付完整流程_第18张图片
微信公众号开发--支付完整流程_第19张图片

注意这行注释,我们知道不能通过get_brand_wcpay_request的值去判断是否支付成功。因为在前端,该代码是有可能被篡改的。更安全的方式是根据后端的异步通知来确定是否支付成功。

在PayController Class中加入一个接受微信异步通知的方法notify。直接使用SDK中的notify处理方法。[代码7]

    @PostMapping("/notify")
    public void notify(@RequestBody String notifyData){
        payService.notify(notifyData);
    }

将异步通知的逻辑写入PayService、PayServiceImpl。[代码8]

    @Override
    public PayResponse notify(String notifyData) {
        PayResponse payResponse = bestPayService.asyncNotify(notifyData);
        log.info("【微信支付】异步通知,payResponse={}", payResponse);
        return payResponse;
    }

同时需要在配置文件application.yml文件中配置notify地址。

微信公众号开发--支付完整流程_第20张图片

支付成功后,需要修改订单的支付状态。也就是更改一下代码8中的内容。[代码9]

    @Override
    public PayResponse notify(String notifyData) {
        PayResponse payResponse = bestPayService.asyncNotify(notifyData);
        log.info("【微信支付】异步通知,payResponse={}", payResponse);

        // 修改订单支付状态
        // 1 先查询一下当前订单状态
        OrderDTO orderDTO = orderService.findOne(payResponse.getOrderId());
        // 2 修改订单状态
        orderService.paid(orderDTO);
        
        return payResponse;
    }

此时,我们可以发现代码安全性不足。在微信异步通知中,有几方面需要注意:

  1. 验证签名(验证一下这个签名是不是真正来自于微信,不然别人模拟一个微信验证请求,我们也会傻fufu的通过)
  2. 支付的状态(虽然会得到异步通知,但是消息的内容不一定是支付成功,也有失败等多种情况)
  3. 支付金额(有可能程序错误,导致微信回调之后的金额不够统一,所以需要校验金额)
  4. 付款人(下单人 == 支付人)(根据业务需要确定下单人和支付人是否一直,所以根据情况可以校验确认一下)

由于使用了SDK,所以第1、2点是不需要我们去做的。代码中我们还需要做第3步。
在判断金额中,要判断微信返回金额与系统金额是否一致,不仅需要保证二者的数据类型相同,也需要精度一致。所以把判断金额这个部分写入了单独的utils
MathUtil.class [代码10]

public class MathUtil {
    private static final Double Money_Range = 0.01;

    public static Boolean equals(Double d1, Double d2){
        Double result =  Math.abs(d1 - d2);
        if (result < Money_Range){
            return true;
        } else {
            return false;
        }
    }
}

完成MathUtil.class之后,我们也需要相应的更改代码9。[代码11]

    @Override
    public PayResponse notify(String notifyData) {
        PayResponse payResponse = bestPayService.asyncNotify(notifyData);
        log.info("【微信支付】异步通知,payResponse={}", payResponse);

        // 修改订单支付状态
        // 1 先查询一下当前订单
        OrderDTO orderDTO = orderService.findOne(payResponse.getOrderId());
        // 2 判断订单是否存在
        if (orderDTO == null) {
            log.error("【微信支付】异步通知,订单不存在。orderId={}", payResponse.getOrderId());
            throw new SellException(ResultEnum.ORDER_NOT_EXIST);
        }
        // 3 判断金额是否一致(因为很多判断中,由于精度的不同,会判断两个金额不一致,比如0.10和0.1;所以采用相减的方式,写在util工具类中)
        if (!MathUtil.equals(payResponse.getOrderAmount(), orderDTO.getOrderAmount().doubleValue())) {
            log.error("【微信支付】异步通知,订单不存在。orderId={}, 微信通知金额={}, 系统金额 ={}", payResponse.getOrderId(), payResponse.getOrderAmount(), orderDTO.getOrderAmount());
            throw new SellException(ResultEnum.WXPAY_NOTIFY_MONEY_VERIFY_ERROR);
        }
        // 4 2、3步都通过后,再修改订单状态
        orderService.paid(orderDTO);

        return payResponse;
    }

根据微信支付业务流程,在支付成功后需要给微信返回“支付通知”,否则将会一直回调PayService 中的notify。如图是微信支付成功的API文档。

微信公众号开发--支付完整流程_第21张图片

在PayController Class中,同发起支付一样,选择返回 ModelAndView模版,完成微信异步通知,也就是完善代码5中的代码。[代码12]

 @PostMapping("/notify")
    public ModelAndView notify(@RequestBody String notifyData) {
        payService.notify(notifyData);
        // 返回给微信处理结果
        return new ModelAndView("pay/success");
    }

到这里,微信公众号支付的流程就全部结束了。

代码:JsonUtil

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class JsonUtil {
    public static String toJson(Object object) {
        GsonBuilder gsonBuilder = new GsonBuilder();
        gsonBuilder.setPrettyPrinting();
        Gson gson = gsonBuilder.create();
        return gson.toJson(object);
    }
}

你可能感兴趣的:(微信公众号开发--支付完整流程)