微信JSAPI支付(一)统一下单

开发准备

参考文档 JSAPI支付开发文档

支付方式

目前微信主流的支付方式有以下6种

方式 说明
付款码支付 付款码支付是用户展示微信钱包内的“刷卡条码/二维码”给商户系统扫描后直接完成支付的模式。主要应用线下面对面收银的场景。
Native支付 Native支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。
JSAPI支付 JSAPI支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。
APP支付 APP支付又称移动端支付,是商户通过在移动端应用APP中集成开放SDK调起微信支付模块完成支付的模式。
H5支付 H5支付主要是在手机、ipad等移动设备中通过浏览器来唤起微信支付的支付产品。
小程序支付 小程序支付是专门被定义使用在小程序中的支付产品。目前在小程序中能且只能使用小程序支付的方式来唤起微信支付。

因为前面做过关于公众号的文章,因此这里主要介绍JSAPI支付,后面的开发等也围绕于此。

JSAPI应用场景有:

  • 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
  • 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付
  • 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付

核心名词

不同于微信公众号的测试开发,可以使用内网穿透,和普通的测试账号等。微信支付要求开发者,必须要有一个已通过验证的真实商户号,且该商户号开通支付功能,以及该商户下有真实的公众号等。

  1. 【微信商户平台】 微信商户平台是微信支付相关的商户功能集合,包括参数配置、支付数据查询与统计、在线退款、代金券或立减优惠运营等功能 平台入口:pay.weixin.qq.com

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

  3. 【微信支付系统】 微信支付系统是指完成微信支付流程中涉及的API接口、后台业务处理系统、账务系统、回调通知等系统的总称。

  4. 【商户证书】 商户证书是微信提供的二进制文件,商户系统发起与微信支付后台服务器通信请求的时候,作为微信支付后台识别商户真实身份的凭据。

  5. 【商户后台系统】 商户后台系统是商户后台处理业务系统的总称,例如:商户网站、收银系统、进销存系统、发货系统、客服系统等,一般关联开发者自己的数据库。

  6. 【签名】 商户后台和微信支付后台根据相同的密钥和算法生成一个结果,用于校验双方身份合法性。签名的算法由微信支付制定并公开,常用的签名方式有:MD5、SHA1、SHA256、HMAC等。

    扫描二维码关注公众号,回复: 7099221 查看本文章
  7. 【支付密码】 支付密码是用户开通微信支付时单独设置的密码,用于确认支付完成交易授权。该密码与微信登录密码不同。

  8. 【Openid】 用户在公众号内的身份标识,不同公众号拥有不同的openid。商户后台系统通过登录授权、支付通知、查询订单等API可获取到用户的openid。主要用途是判断同一个用户,对用户发送客服消息、模版消息等。

申请的核心账户参数:

账户参数说明

邮件中参数 API参数名 详细说明
APPID appid appid是微信公众账号或开放平台APP的唯一标识,在公众平台申请公众账号或者在开放平台申请APP账号后,微信会自动分配对应的appid,用于标识该应用。可在微信公众平台-->开发-->基本配置里面查看,商户的微信支付审核通过邮件中也会包含该字段值。
微信支付商户号 mch_id 商户申请微信支付后,由微信支付分配的商户收款账号。
API密钥 key 交易过程生成签名的密钥,仅保留在商户系统和微信支付后台,不会在网络中传播。商户妥善保管该Key,切勿在网络中传输,不能在其他客户端中存储,保证key不会被泄漏。商户可根据邮件提示登录微信商户平台进行设置。也可按以下路径设置:微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全-->密钥设置
Appsecret secret AppSecret是APPID对应的接口密码,用于获取接口调用凭证access_token时使用。在微信支付中,先通过OAuth2.0接口获取用户openid,此openid用于微信内网页支付模式下单接口使用。可登录公众平台-->微信支付,获取AppSecret(需成为开发者且帐号没有异常状态)。

协议规则

商户接入微信支付,调用API必须遵循以下规则:

传输方式 为保证交易安全性,采用HTTPS传输
提交方式 采用POST方法提交
数据格式 提交和返回数据都为XML格式,根节点名为xml
字符编码 统一采用UTF-8字符编码
签名算法 MD5/HMAC-SHA256
签名要求 请求和接收数据均需要校验签名,详细方法请参考安全规范-签名算法
证书要求 调用申请退款、撤销订单、红包接口等需要商户api证书,各api接口文档均有说明。
判断逻辑 先判断协议字段返回,再判断业务返回,最后判断交易状态

开发中代码配置的参数(实际开发中建议直接在属性文件中配置,便于环境切换)

// 公众号、小程序appid
public static String APP_ID = "xxxxxxxxx"; 
// AppSecret
public static String SECRET = "xxxxxxxxx";
// 商户号
public static final String MCH_ID = "xxxxxxxxx";
// API密钥
public static final String API_KEY = "xxxxxxxxx";
// 网页授权域名,JSAPI支付授权目录,JS接口安全域名
public static final String AUTH_URL = "xxxxxxxxx";
复制代码

以上参数不便公开。如果公司有现成的支付账户最好,没有的话恐怕只能在某宝租用一下了,但没有这些不影响前期的业务开发。

业务梳理

业务流程时序图

对于开发者来说,发起支付的过程中,

后端:主要调用了JSAPI支付中的三个接口:【统一下单API】、【支付结果通知API】、【查询订单API

前端:

前端微信内H5调起支付,提供用户触发微信支付的button和JSON数据传输。

开始开发

项目搭建

一、采用SpringBoot+Thymeleaf结构,参考微信公众号快速开发(二)项目搭建与被动回复

二、引入官方SDK工具包

阅读文档后发现,对于xml解析,加密算法等其实都时常用的方法,微信为我们直接提供了常用工具类方法的半成品,注意,这些只能是半成品,使用时需要做适当的更改。

链接:SDK与DEMO下载,选择JAVA版本下载后解压即可

代码开发

公众号配置

一、将公众号和商户的信息注入到Bean中

@Component
public class WXPayConfigExtend extends WXPayConfig {

    private byte[] certData;

    private WXPayConfigExtend() throws Exception {
//        String certPath = WXPayConstants.APICLIENT_CERT;
//        File file = new File(certPath);
//        InputStream certStream = new FileInputStream(file);
//        this.certData = new byte[(int) file.length()];
//        certStream.read(this.certData);
//        certStream.close();
    }

    @Override
    public String getAppID() {
        return WXPayConstants.APP_ID;
    }
    @Override
    public String getMchID() {
        return WXPayConstants.MCH_ID;
    }
    @Override
    public String getKey() {
        return WXPayConstants.API_KEY;
    }
    @Override
    public InputStream getCertStream() {
        ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
        return certBis;
    }
    @Override
    public int getHttpConnectTimeoutMs() {
        return 2000;
    }
    @Override
    public int getHttpReadTimeoutMs() {
        return 10000;
    }
    @Override
    public IWXPayDomain getWXPayDomain() {
        return WXPayDomainSimpleImpl.instance();
    }
    public String getPrimaryDomain() {
        return "api.mch.weixin.qq.com";
    }
    public String getAlternateDomain() {
        return "api2.mch.weixin.qq.com";
    }
    @Override
    public int getReportWorkerNum() {
        return 1;
    }
    @Override
    public int getReportBatchSize() {
        return 2;
    }
}
复制代码

获取openid

需页面提供网页授权,以获取openid,关于微信网页授权可参考:微信公众号快速开发(四)微信网页授权

页面:

页面这里直接设计了一个可以发起预支付的按钮的静态页面:templates/preOrder.html

里面包含了跳转到后端支付接口的表单:

<form name=wexinpayment action='http://chety.mynatapp.cc/api/v1/wechat1/placeOrder' method=post target="_blank">
    ...
复制代码

Thymeleaf下页面转发的控制器:

@Controller
@RequestMapping("/api/v1/wechat1")
public class IndexController {

    // 用于thymeleaf环境下,跳转到字符串相应的html页面
    @RequestMapping("/{path}")
    public String webPath(@PathVariable String path) {
        return path;
    }
}   
复制代码

网页授权的入口控制器:

@Controller
@RequestMapping("/api/v1/wechat1")
public class IndexController {

    ...

    @RequestMapping("/index")
    public void index(String code, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 显式授权,获得code
        if (code != null) {
            JSONObject json = WeChatUtil.getWebAccessToken(code);
            WXPayUtil.getLogger().info("code: ",json.toJSONString());
            String openid = json.getString(("openid"));
            request.getSession().setAttribute("openid", openid);
            WXPayUtil.getLogger().info("index openid={}",openid);
            // 重定向到预下单页面
            response.sendRedirect("preOrder"); // 重定向到预支付页面
        } else {
            StringBuffer url = RequestUtil.getRequestURL(request);
            WXPayUtil.getLogger().info("index 请求路径:{}"+url);
            String path = WeChatUtil.WEB_REDIRECT_URL.replace("APPID", WeChatConstants.APP_ID).replace("REDIRECT_URI", url).replace("SCOPE", "snsapi_userinfo");
            WXPayUtil.getLogger().info("index 重定向:{}",path);
            // 重定向到授权获取code的页面
            response.sendRedirect(path);
        }
    }
}    
复制代码

启动项目,请求接口:

一、 微信开发者工具的地址栏输入:{网页授权域名}//api/v1/wechat1/index

二、确认【同意】授权,(这里目的是为了获取openid,也可以使用base静默授权的模式,不用显示的提示授权),跳转到预支付页面,如图:

发起支付

当用户确认预支付页面的订单时,将请求【/placeOrder】接口,该业务将调用微信的【统一下单】接口:

一、微信统一下单实体类

@Setter
@Getter
@ToString
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class WxOrderEntity {
    private String appid;
    private String mchId;
    private String deviceInfo;
    private String nonceStr;
    private String sign;
    private String body;
    private String outTradeNo;
    private int totalFee;
    private String spbillCreateIp;
    private String notifyUrl;
    private String tradeType;
    private String openid;
}
复制代码

二、微信支付的业务层

@Service
public class WxBackendServiceImpl {

    @Autowired
    WXPayConfigExtend wxPayConfigExtend;

    // 统一下单
    public Map<String, Object> unifiedorder(Model model, HttpServletRequest request) throws Exception {
        WXPayUtil.getLogger().info("进入下单控制器...");
        Map<String,Object> data = null;
        try {
            //生成订单编号
            WXPay wxpay = new WXPay(wxPayConfigExtend);
            WxOrderEntity order = new WxOrderEntity();

            double price = 0.01;
            String orderName = "xxx--微信支付";
            int number = (int)((Math.random()*9)*1000);//随机数
            DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");//时间
            String orderNumber = dateFormat.format(new Date()) + number;
            String nonceStr = WXPayUtil.generateNonceStr();
            String openId = (String) request.getSession().getAttribute("openid");
            openId = openId == null ? "o4036jqo2PN9isV6N2FHGRsGRVqg" : openId; // 前一个openid,是chet在xxx公众号下的openid

            order.setBody(orderName);
            order.setOutTradeNo(orderNumber);
            order.setTotalFee(MoneyUtil.Yuan2Fen(price));
            order.setSpbillCreateIp(IpUtils.getIpAddr(request));
            order.setOpenid(openId);
            order.setNotifyUrl(WXPayConstants.NOTIFY_URL);
            order.setTradeType(WXPayConstants.TRADE_TYPE_JSAPI);
            order.setNonceStr(nonceStr);

            WXPayUtil.getLogger().info("save 统一下单接口调用,order:{}",order);
            // 利用sdk统一下单,已自动调用wxpay.fillRequestData(data);
            Map<String, String> response = wxpay.doWxPayApi(order,WXPayConstants.UNIFIEDORDER);
            WXPayUtil.getLogger().info("save 下单结果,response:{}",response);

            if(response.get(WXPayConstants.RETURN_CODE).equals("SUCCESS")&&response.get(WXPayConstants.RESULT_CODE).equals("SUCCESS")){
                String url = request.getQueryString() == null?request.getRequestURL().toString():request.getRequestURL()+"?"+request.getQueryString();
                String prepayId = response.get(WXPayConstants.PREPAY_ID);
                data = wxpay.permissionValidate(nonceStr,url,prepayId,wxPayConfigExtend.getKey());
                return data;
            }
        } catch (Exception e) {
            WXPayUtil.getLogger().error("doUnifiedOrder--下单失败:{}" , e.getMessage());
        }
        return null;
    }
}    
复制代码

wxpay.doWxPayApi(...)封装了对下单接口的调用:

public Map<String, String> doWxPayApi(WxOrderEntity order,String apiType) {
    Map<String, String> resp = null;
    try {
        Map<String,String> map = new HashMap<>();
        map.put("out_trade_no", order.getOutTradeNo());
        map.put("nonce_str", order.getNonceStr());
        map.put("trade_type", order.getTradeType());

        if ("unifiedorder".equalsIgnoreCase(apiType)) {
            map.put("spbill_create_ip", order.getSpbillCreateIp());
            map.put("openid", order.getOpenid());
            map.put("notify_url", order.getNotifyUrl());
            map.put("total_fee", String.valueOf(order.getTotalFee()));
            map.put("body", order.getBody());

            resp = unifiedOrder(map);
        } else if ("orderquery".equalsIgnoreCase(apiType)) {
            resp = orderQuery(map);
        } else if ("closeorder".equalsIgnoreCase(apiType)) {
            resp = orderQuery(map);
        }
    } catch (Exception e) {
        WXPayUtil.getLogger().error(order.getOutTradeNo()+" -- 调用接口失败 {}",e.getMessage());
    }
    return resp;
}
复制代码

wxPay.doWxPayApi(...)封装了对签名的二次校验:

public Map<String, Object> permissionValidate(String nonceStr, String url, String prepayId, String key) throws Exception {
    //jssdk权限验证参数
    TreeMap<Object, Object> param = new TreeMap<>();
    Map<String, Object> data = new HashMap<>();
    param.put("appId", WeChatConstants.APP_ID);
    String timestamp = String.valueOf(WXPayUtil.getCurrentTimestamp());
    param.put("timestamp", timestamp);//全小写
    param.put("nonceStr", nonceStr);
    //map.put("signature",WeChatUtil.getSignature(timestamp,uuid,RequestUtil.getUrl(request)));
    param.put("signature", WeChatUtil.getSignature(timestamp, nonceStr, url));
    data.put("configMap", param);

    //微信支付权限验证参数
    Map<String, String> payMap = new HashMap<>();
    payMap.put("appId", WeChatConstants.APP_ID);
    payMap.put("timeStamp", timestamp);//驼峰
    payMap.put("nonceStr", nonceStr);
    payMap.put("package", "prepay_id=" + prepayId);
    payMap.put("signType", "MD5");
    payMap.put("paySign", WXPayUtil.generateSignature(payMap, key));
    payMap.put("packageStr", "prepay_id=" + prepayId);
    data.put("payMap", payMap);

    return data;
}
复制代码

支付结果通知与回调

配置回调接口的控制器:

@Controller
@RequestMapping("/api/v1/wechat1")
public class NotifyController {

    WxBackendServiceImpl wxBackendService;

    /**
     * 在调用下单接口时,我们会传入 异步接收微信支付结果通知的回调地址,顾名思义这个地址作用就是用来接收支付结果通知,
     * 当用户在前端支付成功后,微信服务器会自动调用此地址,然后商户再进行处理
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/wxnotify")
    public String wxNotify(HttpServletRequest request, HttpServletResponse response) {
        String respXml = "";
        try (InputStream in = request.getInputStream();
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = in.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            // 获取微信调用我们notify_url的返回信息
            String notifyData = new String(baos.toByteArray(), "utf-8");
            // 回调处理
            respXml = wxBackendService.payCallBack(notifyData);
        } catch (Exception e) {
            WXPayUtil.getLogger().error("wxnotify:支付回调发布异常:", e.getMessage());
        } finally {
            try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())){
                // 处理业务完毕
                bos.write(respXml.getBytes());
            } catch (IOException e) {
                WXPayUtil.getLogger().error("wxnotify:支付回调发布异常:out:", e.getMessage());
            }
        }
        return respXml;
    }
}
复制代码

回调业务:

public String payCallBack(String notifyData) throws Exception{
    // String respXml = WXPayConstants.RESP_FAIL_XML;
    Map<String, String> notifyMap = WXPayUtil.xmlToMap(notifyData);

    if (WXPayConstants.SUCCESS.equalsIgnoreCase(notifyMap.get(WXPayConstants.RESULT_CODE))) {
        WXPayUtil.getLogger().info("payCallBack:微信支付----返回成功");
        if (WXPayUtil.isSignatureValid(notifyMap, WXPayConstants.API_KEY)) {
            // TODO 数据库操作,付款记录修改 & 记录付款日志
            WXPayUtil.getLogger().info("payCallBack:微信支付----验证签名成功,更新数据库");
            /*String outTradeNo = notifyMap.get("out_trade_no");
            OrderTrading dbOrder = transactionService.findByOutTradeNo(outTradeNo);
            // 将未支付状态改为已支付
            if (dbOrder != null && dbOrder.getState() == 1) {
                // 处理业务 - 修改订单状态
                OrderTrading order = new OrderTrading();
                order.setOutTradeNo(outTradeNo);
                order.setNotifyTime(new Date());
                order.setState(1);

                transactionService.updateTransOrderByWxnotify(order);
                // TODO 数据库更新异常,补偿措施
            }*/
            // 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
            return WXPayConstants.RESP_SUCCESS_XML;
        } else {
            WXPayUtil.getLogger().error("payCallBack:微信支付----判断签名错误");
        }
    } else {
        WXPayUtil.getLogger().error("payCallBack:支付失败,错误信息:" + notifyMap.get(WXPayConstants.ERR_CODE_DES));
    }
    return WXPayConstants.RESP_FAIL_XML;
}
复制代码

静态页面

预下单页面:templates/preOrder.html

确认下单页面:templates/toOrder.html

该页面用于签名校验和参数传递,为便于观察,开启了调试模式

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>立即支付:123</h1>

<button type="submit" id="payBtn">支付</button>

<script th:src="@{/static/js/jquery-1.8.3.min.js}" type="text/javascript" charset="utf-8" rel="stylesheet"></script>
<script type="text/javascript" th:src="@{/static/js/jquery.rotate.min.js}" rel="stylesheet"></script>
<!--微信的JSSDK-->
<script th:src="@{http://res.wx.qq.com/open/js/jweixin-1.2.0.js}"></script>
<script>
    $(function() {
        <!--通过config接口注入权限验证配置-->
        alert('[[${configMap}]]');
        alert('[[${payMap}]]');
        wx.config({
            debug: true, // 开启调试模式
            appId: '[[${configMap.appId}]]', // 公众号的唯一标识
            timestamp: '[[${configMap.timestamp}]]', // 生成签名的时间戳
            nonceStr: '[[${configMap.nonceStr}]]', // 生成签名的随机串
            signature: '[[${configMap.signature}]]',// 签名
            jsApiList: ['chooseWXPay'] // 填入需要使用的JS接口列表,这里是先声明我们要用到支付的JS接口
        });

        <!-- config验证成功后会调用ready中的代码 -->
        wx.ready(function(){
            //点击马上付款按钮
            $("#payBtn").click(function(){              
                //弹出支付窗口
                wx.chooseWXPay({
                    timestamp: '[[${payMap.timeStamp}]]', // 支付签名时间戳,
                    nonceStr: '[[${payMap.nonceStr}]]', // 支付签名随机串,不长于 32 位
                    package: '[[${payMap.packageStr}]]', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=xxxx)
                    signType: '[[${payMap.signType}]]', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
                    paySign: '[[${payMap.paySign}]]', // 支付签名
                    success: function (res) {
                        // 支付成功后的回调函数
                        alert("支付成功!");
                    }
                });
            })
        });
    });
</script>
</body>
</html>
复制代码

效果演示

项目启动后,点击确认支付就可以看看到debug模式下参数的显示了。最后的支付效果如图:

注:

  1. 支付回调的端口必须是80,应该是出于安全考虑
  2. web开发工具只能用于调试,测试支付功能时,需要用手机打开。
  3. 细心的朋友可能看出来,订单的时间早了一个多月。这个是我之前用公司账号和域名开发的,用的当时的截图。

猜你喜欢

转载自juejin.im/post/5d6297c95188255d51426d50