美文网首页
微信公众号开发--支付完整流程

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

作者: NnnLillian | 来源:发表于2019-09-29 17:18 被阅读0次

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

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

获取OpenId

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


手工方式

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

普通商户接入文档界面
打开链接可以看到JSAPI支付的详细内容,接下来的操作都是根据JSAPI支付中的“业务流程”逐步完成。
JSAPI支付详细
PS.其实下图链接网页授权获取用户openid接口文档也是真的写的很清楚了。当trade_type=JSAPI时(即公众号支付),openId必传。
openId的重要性
1. 设置网页授权域名

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


填写完域名之后,记得下载文件,因为规定很多,还需要ICP备案什么的,作为调试,我选择使用了https://natapp.cn/穿透内网,就可以使得微信这边访问到自己的电脑。

在NATAPP中,注册/登录,购买隧道,免费的只能临时用一下,还会随便换域名/端口,所以我购买了9/月的。

查看购买到的隧道,这个域名是你在购买过程中,他让你自己输入的。

查看教程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。 参数说明
用户同意授权后,跳转到redirect_uri并返回code。 用户同意授权
3. 换取access_token
获取了code之后,以code作为票据再换access_token。以下是微信文档的相应截图,我们只需要了解的就是替换掉链接中的appId为自己的appId和code为刚刚后台获取的code(注:code时效只有5min)。 换取access_token
4. 得到openId
在上一个步骤中,获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。请求上述链接时,正确时会返回如下JSON数据包。其中就包含了openId。 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]

<dependency>
  <groupId>com.github.binarywang</groupId>
  <artifactId>weixin-java-mp</artifactId>
  <version>3.5.0</version>
</dependency>

在本地函数中的使用,主要查看文档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;
    }
}
配置文件

微信账号相关的部分先写一个配置文件。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]

<dependency>
  <groupId>cn.springboot</groupId>
  <artifactId>best-pay-sdk</artifactId>
  <version>1.1.0</version>
</dependency>

新建一个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;
}
同时修改给配置文件增加相应内容

配置一下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支付开发者文档
需要向后端先传递这些参数。

之后会返回如下图所示的前端代码,这个代码就是最后生成微信支付页的部分。

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

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

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

完善之前的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<String, Object> 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调起支付
文档中返回的代码格式

<script>
    function onBridgeReady() {
        WeixinJSBridge.invoke(
            'getBrandWCPayRequest', {
                "appId":"${payResponse.appId}",     //公众号名称,由商户传入
                "timeStamp":"${payResponse.timeStamp}",         //时间戳,自1970年以来的秒数
                "nonceStr":"${payResponse.nonceStr}", //随机串
                "package":"${payResponse.packAge}",
                "signType": "MD5",         //微信签名方式:
                "paySign":"${payResponse.paySign}" //微信签名
            },
            function (res) {
                // if (res.err_msg == "get_brand_wcpay_request:ok") {
                // }     // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回    ok,但并不保证它绝对可靠。
                location.href="${returnUrl}"
            }
        );
    }

    if (typeof WeixinJSBridge == "undefined") {
        if (document.addEventListener) {
            document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
        } else if (document.attachEvent) {
            document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
            document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
        }
    } else {
        onBridgeReady();
    }
</script>
此时已经完成动态注入参数了,但是完成支付还需要我们在前端文件中配置一下,参考之前的【前端调试】模块。

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

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


微信异步通知

在微信内H5调起支付时,前端也可以接收到一个是否成功的标志。

注意这行注释,我们知道不能通过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地址。


支付成功后,需要修改订单的支付状态。也就是更改一下代码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文档。


在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);
    }
}

相关文章

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

    微信公众号支付的完整流程,首先需要微信授权,获取openId,因为openid是微信用户在公众号appid下的唯一...

  • PHP微信公众号支付开发总结

    微信公众号开发是很常见的开发,其中微信支付占有很大的比重。微信公众号分为服务号和订阅号,服务号支持微信支付。 前期...

  • 微信公众号配置

    微信支付 - 商户平台 商户号 支付证书 微信公众号 appid & appscret 开发 > 基本配置 > ...

  • 微信公众号网页支付summer版

    该文仅对于中间这种支付方式有参考价值哟 一、开发背景 在微信公众号中,需要进行微信支付且为微信公众号网页支付。 二...

  • 收录 : iOS支付开发

    iOS 银联支付开发流程iOS 微信支付开发流程iOS 支付宝支付开发流程iOS Apple Pay开发流程App...

  • 微信支付流程&&返回-1

    微信支付开发完整流程与-1”闪屏”解决 文档说明 现在的APP里面只要有支付的应该都会用到微信支付,但是在开发的过...

  • 微信公众号开发之现金红包

    欢迎留言、转发 微信极速开发系列文章:点击这里 前几篇文章介绍了微信支付。 公众号支付、微信扫码支付、刷卡支付、微...

  • 微信支付开发(一)如何配置支付授权目录

    背景 开发公众号支付,需要在微信商户号内配置支付授权目录,才能在微信内唤起微信支付。配置方法如下。 设置路径 登录...

  • 2018-01-15

    微信公众号支付的进坑之路,希望能初次开发的朋友们少点弯路 本人初次开发微信公众号支付,看官方文档,网上查找相关资料...

  • 微信公众号支付流程

    处理兼容 支付配置 啊哈,前端部分工作就这些。

网友评论

      本文标题:微信公众号开发--支付完整流程

      本文链接:https://www.haomeiwen.com/subject/zdifpctx.html