微信支付

微信支付(公众号)开发流程

  • openId是每个用户在公众号中的唯一编号,商户后台获取预支付单时要用到openId(参与MD5计算)

  • 获取openId之前,应先获取code码

  • 注意设置微信支付商户平台的支付授权目录,设置的路径应该为:支付页面的上一级目录,如支付页面url为:www.xxx.com/app/pay.html,那么,支付授权目录则为:www.xxx.com/app/,记得以“/”结尾

总体流程

  1. 前端获取code码
  2. 后端使用code获取用户openId
  3. 商户后台拿到openId,整合其他参数后向微信服务器发起预支付请求,得到预支付单
  4. 商户后台抽取预支付单的参数,整合其他参数后将数据返回给用户微信客户端
  5. 用户微信客户端使用返回的参数调起微信支付内置方法,若参数正确,将弹出支付窗口,用户输入密码完成支付

细节

1、前端获取code码
function userPay() {
    var redirect_uri = 'https://localhost:8488/emsb/APP/userPay.html';
    //调用网页授权接口链接,注意,redirect_uri必须进行encode
    var url = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxxxxxx&redirect_uri='+encodeURI(redirect_uri)+
        '&response_type=code&scope=snsapi_base&state=1#wechat_redirect';
    //执行跳转
    window.location = url;
}

执行此方法,注意

  • 将url 中 appid换成自己公众号的appid

  • redirect_uri 应进行encodeURI编码

  • scope=snsapi_base:获取用户基本信息:openId,不需要用户授权

  • scope=snsapi_userinfo:获取openId和其他用户资料(昵称、头像、国、省、城市、性别、权限),需要用户手动授权

  • window.location 执行完成后,将自动跳转到redirect_uri页面,并将所请求的信息附加在url地址后方,可使用以下方法获取

    let url = location.search.substr(1);
    let list = url.split("&");
    let code = list[0].split("=")[1];
    let state = list[1].split("=")[1];
    
2、通过code码获取openId

因获取openId时还需要使用一些敏感的参数(appsecret),应将此步骤放到商户后台

String appid = "xxxxxxxxxx";
String appsecret = "xxxxxxxxxxxx";

//使用HttpClient
String praiseUrl = "https://api.weixin.qq.com/sns/oauth2/access_token";
HttpClient client = new HttpClient();
PostMethod postMethod = new PostMethod(praiseUrl);

// 必须设置下面这个Header
postMethod.addRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36");
postMethod.addParameter("appid", appid);
postMethod.addParameter("secret", appsecret);
postMethod.addParameter("code", code);
postMethod.addParameter("grant_type", "authorization_code");

//发送请求,并获取返回结果
int res_code = client.executeMethod(postMethod);
if (res_code == 200) {
    String res = postMethod.getResponseBodyAsString();
    
    //转为Map,以便获取openId
    responseMap = (Map<String, String>) JSON.parse(res);
    //responseMap.get("openid");
}

这里使用HttpClient向微信接口发送请求,应在pom.xml中添加如下依赖

<dependency>
    <groupId>commons-httpclient</groupId>
    <artifactId>commons-httpclient</artifactId>
    <version>3.1</version>
</dependency>

同时使用到了JSON,在pom.xml中添加如下依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
</dependency>
3、向微信接口请求预支付单

此过程较复杂,为节省时间,直接用微信官方的SDK:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1,下载java版本后,将 src\main\java\com\github\wxpay\sdk 包下所有文件导入当前工程,并引入pom.xml中所有的依赖

  • IWXPayDomain
  • MyConfig
  • WXPay
  • WXPayConfig
  • WXPayConstants
  • WXPayReport
  • WXPayRequest
  • WXPayUtil
  • WXPayXmlUtil

根据REAEME.md中的提示,新建MyConfig继承WXPayConfig,实现其中的方法

public class MyConfig extends WXPayConfig {

    private byte[] certData;

    public MyConfig() throws Exception {
        try {
            ClassPathResource classPathResource = new ClassPathResource("config/apiclient_cert.p12");
            //获取文件流
            InputStream certStream = classPathResource.getInputStream();
            this.certData = IOUtils.toByteArray(certStream);
            certStream.read(this.certData);
            certStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    String getAppID() {
        return "xxxxxxxxxxxxxxx";
    }

    //商户号
    @Override
    String getMchID() {
        return "xxxxxxxxxxxxxx";
    }

    //key 密钥 在微信支付商户平台->产品中心->开发配置中设置支付密钥
    @Override
    String getKey() {
        return "xxxxxxxxxxxxxxxx";
    }

    @Override
    InputStream getCertStream() {
        ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
        return certBis;
    }

    @Override
    IWXPayDomain getWXPayDomain() {
        IWXPayDomain iwxPayDomain = new IWXPayDomain() {
            @Override
            public void report(String domain, long elapsedTimeMillis, Exception ex) {

            }
            @Override
            public DomainInfo getDomain(WXPayConfig config) {
                return new IWXPayDomain.DomainInfo(WXPayConstants.DOMAIN_API, true);
            }
        };
        return iwxPayDomain;
    }

    public int getHttpConnectTimeoutMs() {
        return 8000;
    }

    public int getHttpReadTimeoutMs() {
        return 10000;
    }
}

平常使用SpringBoot打包后,外部证书文件获取可能失败,这里已经做了优化,使用IOUtils,应导入如下依赖

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-io -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-io</artifactId>
    <version>1.3.2</version>
</dependency>
4、获取预支付单
MyConfig config = new MyConfig();
            WXPay wxpay = new WXPay(config);
            Map<String, String> data = new HashMap<>();
            data.put("appid", "xxxxxxxxxx");
            data.put("mch_id", "xxxxxxxx");
            data.put("nonce_str", WXPayUtil.generateNonceStr());	//随机串,这里使用了微信SDK提供的工具
            data.put("sign_type", "MD5");
            data.put("body", "1");
            data.put("out_trade_no", WXPayUtil.generateNonceStr());
            data.put("total_fee", "1");
            data.put("spbill_create_ip", "123.12.12.123");	//暂时随便指定,不影响
            data.put("notify_url", "http://www.baidu.com");		//暂时随便指定,不影响
            data.put("trade_type", "JSAPI");	//公众号支付为JSAPI
            data.put("device_info", "WEB");		//公众号支付为WEB
            data.put("openid", responseMap.get("openid"));	//拿到用户的openId

            //为订单信息签名,防止篡改:签名的内容为以上data中的全部数据,注意map中key的大小写
            String sign = WXPayUtil.generateSignature(data, "xxxxxxxxxxxxxxxxxxx", WXPayConstants.SignType.MD5);
			//将签名放入map中,一并发送给微信接口
            data.put("sign", sign);
			//调用微信统一下单接口,接收返回数据(Map),其中包含用户支付的核心数据
            Map<String, String> resp = wxpay.unifiedOrder(data);

可以到https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=20_1测试数据签名是否正确

5、为预支付单数据签名,发送给用户微信客户端
//签名返回数据
Map<String, String> map = new HashMap<>();

//注意resp为调用统一下单接口后的返回数据map
//注意key的大小写
map.put("appId", resp.get("appid"));
map.put("package", "prepay_id=" + resp.get("prepay_id"));
map.put("nonceStr", resp.get("nonce_str"));
map.put("signType", "MD5");
map.put("timeStamp", String.valueOf(WXPayUtil.getCurrentTimestamp()));	//时间戳单位为秒,直接使用SDK提供的工具
//使用支付密钥为以上五条数据签名,签名类型为MD5,防止数据被篡改
String paySign = WXPayUtil.generateSignature(map, "xxxxxxxxxxx", WXPayConstants.SignType.MD5);
//将签名一并发送给客户端
map.put("paySign", paySign);

//将map返回给前台
return map;
6、微信客户端调起支付窗口

这是从微信官方文档中拿到的代码

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

//以下可以注释掉,手动调用onBridgeReady方法即可,若参数校验正确,将拉起支付密码框
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();
}

到这里之后,可能即使你经过反复确认,数据完全正确,也无法成功拉起支付密码输入框,系统提示支付验证失败之类的,其实,微信的官方SDK中有一个坑:找到 WXPay.java

public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox) throws Exception {
        this.config = config;
        this.notifyUrl = notifyUrl;
        this.autoReport = autoReport;
        this.useSandbox = useSandbox;
        if (useSandbox) {
            this.signType = SignType.MD5; // 沙箱环境
        }
        else {
//            this.signType = SignType.HMACSHA256;
            this.signType = SignType.MD5;
        }
        this.wxPayRequest = new WXPayRequest(config);
    }

在这个构造方法中,

我注释了 this.signType = SignType.HMACSHA256; ,

应改为this.signType = SignType.MD5;

现在应该能够成功调起支付窗口了。

posted @ 2023-06-14 09:28  谭五月  阅读(157)  评论(0)    收藏  举报