文章目录

  • 微信公众平台开发:微信网页授权和微信支付
  • 1. 微信网页授权
  • 1.1 微信网页授权和微信支付有什么关系?
  • 1.2 微信授权流程
  • 1.2.1 手工实现微信网页授权
  • 1.2.2 利用第三方SDK开发
  • 2. 微信支付
  • 2.1 微信支付场景
  • 2.2 JSAPI支付
  • 2.3 微信支付业务流程
  • 2.4 代码实现
  • 2.4.1 统一下单接口
  • 2.4.2 微信内H5调起支付
  • 2.4.3 异步通知商户支付结果


微信公众平台开发:微信网页授权和微信支付

问题1:微信公众平台网页授权和微信开放平台网页授权什么关系?

① 微信公众平台

微信公众平台是微信公众账号申请入口和管理后台。商户可以在公众平台提交基本资料、业务资料、财务资料申请开通微信支付功能。

平台入口:http://mp.weixin.qq.com

② 微信开放平台

微信开放平台是商户APP接入微信支付开放接口的申请入口,通过此平台可申请微信APP支付

平台入口:http://open.weixin.qq.com

问题2:微信支付和微信授权什么关系?

这篇文章,主要想聊一下微信公众平台的开发,下一篇聊微信开放平台授权

1. 微信网页授权

上一篇文章中已经详细的讲了微信网页授权的开发步骤,这里结合上篇文章,主要讲讲两种授权方式

1.1 微信网页授权和微信支付有什么关系?

① 如果用户第一次使用美团外卖的公众号,进入美团外卖公众号后选择一家小店的商品加入购物车,然后点击去结算

②点击结算后,出现一个要求用户授权的界面(在公众号进行微信支付功能,目的是获取用户的openid,只有拥有用户的openid才能进行后序的微信支付行为)

③ 当用户同意授权后,这时就会微信支付系统就会创建一个预付单

④ 点击去支付,完成微信支付

微信开发工具怎么微信支付 微信开放平台微信支付_微信支付

1.2 微信授权流程

授权流程主要涉及前面两个步骤:

微信开发工具怎么微信支付 微信开放平台微信支付_微信_02

关于微信授权的流程上一篇有具体详细的解释,这里梳理大致思路:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html

① 用户同意授权,获取Code

  1. 引导用户访问:https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect (参数官方文档,很清楚)
  2. 当用户访问这个连接时就会跳转到 图② 微信网页授权页面,即美团外卖申请获取你的公开信息。
  3. 当用户点击允许后,代表同意授权,页面将跳转到: redirect_uri/?code=CODE&state=STATE。

② 通过code换取网页授权access_token(获取access_token的同时还会获取openid)

  1. redirect_uri/?code=CODE&state=STATE这个请求的请求参数中可以获得code
  2. 获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

③ 通过access_token和openid拉取用户信息(要求scope=snsapi_userinfo)

  1. 从请求的返回结果中,便可以获得access_token 和 openid
  2. 如果网页授权作用域为snsapi_userinfo,则此时开发者可以通过access_token和openid拉取用户信息了。
  3. 请求方法:https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
  4. 从请求方法的返回结果即可得到用户的信息

1.2.1 手工实现微信网页授权

@Controller
@RequestMapping("/weixin")
public class WiexinOathu {

    // 1、当用户请求这个方法时,我们需要引导用户打开url这个地址,请求授权
    @RequestMapping("/oauth")
    public void autho(HttpServletResponse response)  {
        //redirect_uri
        String path = "http://heng.nat300.top/sell/weixin/invoke";
        try {
            URLEncoder.encode(path,"UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        String url = "https://open.weixin.qq.com/connect/oauth2/authorize?" +
            "appid=wxd0430ca3dde79d52" +
            "&redirect_uri="+ path +
            "&response_type=code" +
            "&scope=snsapi_userinfo" +
            "&state=STATE" +
            "#wechat_redirect";

        try {
            response.sendRedirect(url);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //用户同意授权后,页面回调接口路径:http://heng.nat300.top/sell/weixin/invoke
    @RequestMapping("/invoke")
    public void oauthInvoke(HttpServletRequest request){
        //2、从回调地址中获取code
        String code = request.getParameter("code");
        String state = request.getParameter("state");

        System.out.println("code="+code);

        //3、认证服务器,获取code后,请求以下链接获取网页授权access_token
        String url = "https://api.weixin.qq.com/sns/oauth2/access_token?" +
                "appid=wxd0430ca3dde79d52" +
                "&secret=46088f58a30ffb26b12d921735d26691" +
                "&code="+code +
                "&grant_type=authorization_code";

        //认证服务器响应的json数据,获取access_token 和 openid
        JSONObject jsonObject = WeiXinUtil.httpRequest(url,"POST",null);
        String access_token = jsonObject.getString("access_token");
        String openid = jsonObject.getString("openid");

        System.out.println("access_token="+access_token);
        System.out.println("openid="+openid);

        //4、根据openid和access_token获取资源信息
        String URLUserinfo = "https://api.weixin.qq.com/sns/userinfo?" +
                "access_token=" + access_token +
                "&openid="+openid+
                "&lang=zh_CN";
        JSONObject jsonobj = WeiXinUtil.httpRequest(URLUserinfo,"GET",null);
        System.out.println(jsonobj);
    }
}

在微信客户端访问:http://heng.nat300.top/sell/weixin/oauth

code=061Lgdll21xhn54RXCnl2EO7F33Lgdlg

---->httpRequest传回的信息是:{"access_token":"36_Sf82qk9BPk-3cQmqlmmqlQNtBKWcd6Qldslgj75fRzhw0rkzmlLWvd8NLUKodlmhavSq3CVaMyvOjjIsQufKUw","expires_in":7200,"refresh_token":"36_TOv7gFw840QLTNe_GmzAA6sDtDOo-2TlpkzuAM8t5GXVhu5SXSpOMhW83tYu9BqsJdxRb8w7p90oEK_1Uhz7FA","openid":"oaaZ7txEYH7kpwRVfBCc9hych-CE","scope":"snsapi_userinfo"}

access_token=36_Sf82qk9BPk-3cQmqlmmqlQNtBKWcd6Qldslgj75fRzhw0rkzmlLWvd8NLUKodlmhavSq3CVaMyvOjjIsQufKUw

openid=oaaZ7txEYH7kpwRVfBCc9hych-CE

---->httpRequest传回的信息是:{"openid":"oaaZ7txEYH7kpwRVfBCc9hych-CE","nickname":"别惹我","sex":2,"language":"zh_CN","city":"南京","province":"江苏","country":"中国","headimgurl":"http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoMGGJX7Etibc0he241duGKMSWickeibOUAGEC0OeaxuzMXaAOhbK3KSBViaNibf4ZbTBoABTqgSc0ia3bA/132","privilege":[]}
{"openid":"oaaZ7txEYH7kpwRVfBCc9hych-CE","nickname":"别惹我","sex":2,"language":"zh_CN","city":"南京","province":"江苏","country":"中国","headimgurl":"http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoMGGJX7Etibc0he241duGKMSWickeibOUAGEC0OeaxuzMXaAOhbK3KSBViaNibf4ZbTBoABTqgSc0ia3bA/132","privilege":[]}

1.2.2 利用第三方SDK开发

第三方SDKGithub地址:https://github.com/Wechat-Group/WxJava/wiki/MP_OAuth2%E7%BD%91%E9%A1%B5%E6%8E%88%E6%9D%83

@Controller
@RequestMapping("/wechat")
@Slf4j
public class WeChatController {
    @Autowired
    private WxMpService wxMpService;

    @Autowired
    private WxMpService wxOpenService;

    //http://heng.nat300.top/sell/wechat/authorize
    @GetMapping("/authorize")
    public String authorize(){
        //returnUrl就是用户授权同意后回调的地址
        String returnUrl="http://heng.nat300.top/sell/wechat/userInfo";
        //引导用户访问这个链接,进行授权
        String url = wxMpService.oauth2buildAuthorizationUrl(returnUrl, WxConsts.OAUTH2_SCOPE_USER_INFO, URLEncoder.encode(returnUrl));
        return "redirect:"+url;
    }

    //用户授权同意后回调的接口,从请求参数中获取code
    @GetMapping("/userInfo")
    public String userInfo(@RequestParam("code")String code,@RequestParam("state")String returnUrl){
        WxMpOAuth2AccessToken wxMpOAuth2AccessToken = new WxMpOAuth2AccessToken();
        try {
            //通过code换取access_token
            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();
        log.info("openid={}", openId);

        //这个地址可有可无,反正只是为了拿到openid,但是如果没有会报400错误,为了好看随便返回一个百度的地址
        returnUrl = "http://www.baidu.com";

        return "redirect:" + returnUrl;
    }
}

在微信客户端访问:http://heng.nat300.top/sell/wechat/authorize

控制台打印openid,同时网页最终返回至returnUrl = “http://www.baidu.com”;

微信开发工具怎么微信支付 微信开放平台微信支付_微信_03

2. 微信支付

开发JSAPI支付时,在统一下单接口中要求必传用户openid,在微信授权时我们已经获取了用户的openid,下面就可以进行微信支付的流程了。

2.1 微信支付场景

商户已有H5商城网站,用户通过消息或扫描二维码在微信内打开网页时,可以调用微信支付完成下单购买的流程。

① 商户通过自定义菜单吸引用户点击进入商户网页, 用户选择购买,完成选购流程,创建预支付订单;

② 调起微信支付控件,用户开始输入支付密码;

③ 密码验证通过,支付成功。商户后台得到支付成功的通知。

④ 返回商户页面,显示购买成功。该页面由商户自定义

⑤ 微信支付公众号下发支付凭证。

⑥ 商户公众号下发消息,提示发货成功。该步骤可选。

微信开发工具怎么微信支付 微信开放平台微信支付_微信开发工具怎么微信支付_04

以下是支付场景的交互细节,请认真阅读,设计商户页面的逻辑:

① 用户打开商户网页选购商品,发起支付,在网页通过JavaScript调用getBrandWCPayRequest接口,发起微信支付请求,用户进入支付流程。

② 用户成功支付点击完成按钮后,商户的前端会收到JavaScript的返回值。商户可直接跳转到支付成功的静态页面进行展示。

③ 商户后台收到来自微信开放平台的支付成功回调通知,标志该笔订单支付成功。

2.2 JSAPI支付

JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。应用场景有:

线下场所:调用接口生成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付

公众号场景:用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付

PC网站场景:在网站中展示二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付

如没有设置支付目录,无法调起支付接口:

微信开发工具怎么微信支付 微信开放平台微信支付_网页授权_05

2.3 微信支付业务流程

① 商户生成图文消息或二维码展示为用户,用户点击消息或扫描二维码在微信浏览器里面打开H5网页中通过JavaScript调用getBrandWCPayRequest接口,发起微信支付请求,请求生成支付订单。

https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6

② 商户后台生成商户订单(业务订单),并调用微信支付系统的统一下单接口API生成预支付单(支付订单),返回预支付单信息(商户根据公众号相关信息、用户相关信息、商品相关信息,去获取一个prepay_id和paySign)

https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1

③ 商户将获取到的前端所需的支付参数,传给前端用户,用户点击发起支付,微信支付系统验证参数合法性等信息后返回验证结果,要求用户支付授权(确认支付,输入密码)

④ 用户输入密码后,微信支付系统验证授权并异步通知商户支付结果(用户已经成功下单并支付了,你赶紧处理一下)

⑤ 商户处理完成后(将订单状态修改为已支付),需要将处理结果告知微信支付系统,否则微信支付系统会一直向商户发送异步通知

https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7

⑥ 微信支付公众号下发支付凭证,返回支付结果,展示消息给用户

微信开发工具怎么微信支付 微信开放平台微信支付_微信_06

可能会出现问题的地方:

问题1:当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知,怎么办?

支付完成后,微信会把相关支付结果及用户信息通过数据流的形式发送给商户,商户需要接收处理,并按文档规范返回应答。

1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。

2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起多次通知),但微信不保证通知最终一定能成功。

3、在订单状态不明或者没有收到微信支付结果通知的情况下,建议商户主动调用微信支付【查询订单API】确认订单状态。

因此,由于网络异常或者系统的波动,可能会导致用户支付成功,但是商户侧未能成功接收到支付结果通知,进而显示订单未支付的情况。商户侧的订单状态更新不及时,容易造成用户投诉,甚至是重复支付的情况发生。

解决方案:

微信开发工具怎么微信支付 微信开放平台微信支付_微信开发工具怎么微信支付_07

① 商户APP或者前端页面收到支付返回时,商户需要调用商户查单接口确认订单状态,并把查询结果展示给用户。

② 商户后台需要准确、高效地处理微信支付发送的异步支付结果通知,并按接口规范把处理结果返回给微信支付。

③ 商户后台未收到异步支付结果通知时,商户应该主动调用《微信支付查单接口》,同步订单状态。

④ 商户在T+1日从微信支付侧获取T日的交易账单,并与商户系统中的订单核对。如出现订单在微信支付侧成功,但是在商户侧未成功的情况,商户需要给用户补发货或者退款处理。

如果商户查单接口返回订单未支付,需要提醒用户“稍后进入订单管理页核实订单状态,不要重复发起支付”。商户后端需要及时获取、更新订单状态,实现逻辑参考【后端服务处理】。当用户再次进入订单管理页面,对未支付的订单再次发起支付时,商户应该使用原单号发起,不要更换支付单号,避免用户重复支付。

2.4 代码实现

2.4.1 统一下单接口

@Slf4j
@Service
public class PayServiceImpl implements PayService {
    //封装了的方法,也是很关键的方法
    @Autowired
    private BestPayServiceImpl bestPayService;

    private static final String ORDER_NAME="微信购物订单";

    //创建一个支付订单,注意是支付订单,不是业务订单
    @Override
    public PayResponse create(OrderDTO orderDTO) {
        //封装支付请求参数
        PayRequest payRequest = new PayRequest();
        //openid
        payRequest.setOpenid(orderDTO.getBuyerOpenid());
        //订单总金额OrderAmount
        payRequest.setOrderAmount(orderDTO.getOrderAmount().doubleValue());
		//订单id
        payRequest.setOrderId(orderDTO.getOrderId());
        //订单名称
        payRequest.setOrderName(ORDER_NAME);
        //支付类型,代表微信公众号支付
        payRequest.setPayTypeEnum(BestPayTypeEnum.WXPAY_H5);

        //调用支付接口发起支付
        bestPayService.pay(payRequest);
        log.info("【微信支付】request = {}", JsonUtil.toJson(payRequest));
		
        //支付后的响应结果
        PayResponse payResponse = bestPayService.pay(payRequest);
        log.info("【微信支付】response】= {}",JsonUtil.toJson(payResponse));
        return payResponse;
    }
}
@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) throws UnsupportedEncodingException {
        //1.根据订单id查询订单
        OrderDTO orderDTO = orderService.findOnes(orderId);
        if(orderDTO==null){
            throw new SellException(ResultEnum.ORDER_NOT_EXIST);
        }

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

        //跳转到模板引擎页面create.ftlh
        return new ModelAndView("pay/create",map);
    }
}

2.4.2 微信内H5调起支付

<script>
    function onBridgeReady(){
    	//动态的从ModelAndView中提取
        WeixinJSBridge.invoke(
            'getBrandWCPayRequest', {
                //公众号名称,由商户传入
                "appId": "${payResponse.appId}",
                //时间戳,自1970年以来的秒数
                "timeStamp": "${payResponse.timeStamp}",
                //随机串
                "nonceStr": "${payResponse.nonceStr}",
                "package":"${payResponse.packAge}",
                //微信签名方式
                "signType": "MD5",
                //微信签名
                "paySign": "${payResponse.paySign}"
            },
            function(res){
                // 在用户支付成功后跳转的通知地址
                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>

2.4.3 异步通知商户支付结果

@Service
@Slf4j
public class PayServiceImpl implements PayService {

    @Autowired
    private BestPayServiceImpl bestPayService;

    @Autowired
    private OrderService orderService;

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

        //查询订单
        OrderDTO orderDTO = orderService.findOne(payResponse.getOrderId());

        //判断订单是否存在
        if (orderDTO == null) {
            log.error("【微信支付】异步通知, 订单不存在, orderId={}", payResponse.getOrderId());
            throw new SellException(ResultEnum.ORDER_NOT_EXIST);
        }

        //判断金额是否一致(0.10   0.1)
        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);
        }

        //修改订单的支付状态
        orderService.paid(orderDTO);

        return payResponse;
    }
}
@Controller
@RequestMapping("/pay")
public class PayController {
    @Autowired
    private OrderService orderService;
    @Autowired
    private PayService payService;
    
	// 微信异步通知
    @PostMapping("/notify")
    public ModelAndView notify(@RequestBody String notifyData) {
        payService.notify(notifyData);
          //商户返回给微信支付系统处理结果
    	return new ModelAndView("pay/success");
    }
}