微信支付平台开发教程(基于JSAPI支付机制)

2021年08月29日 99 字 教程整理


……

0.1. 微信支付(商户)账号开发的一些相关概念:商户号(mchid)、服务器证书(apiclient_cert.p12) 和支付异步回调(notify_url)

0.1.1. 商户号和商户秘钥:mchid和mchKey

0.1.1.1. 微信支付商户号(mchid)

登录微信支付商户平台: https://pay.weixin.qq.com/ ,点击【账户中心】-【商户信息】,即可查看当前账号对应的微信支付商户号(mchid):

0.1.1.2. 微信支付商户秘钥(mchKey)

设置商户秘钥之前,需先安装好操作证书 .从【账户中心】-【API安全】项,进入安装操作证书的向导,完成证书的安装:

操作证书安装好,即可开始配置商户秘钥(亦:API秘钥):

在这里,商户秘钥(mchKey)需要用户自己设置为32位字符并自行保存和记录(密钥生成后不会在微信后台明文展示, 如有遗忘可以人工重置);设置好的秘钥需配置到相关应用的配置文件中,这是微信支付底层很关键的一个参数,核心功能基本都会涉及这个参数的调用或校验。
另外需注意,如果微信支付平台更改了密钥,更改成功后一定要重新下载证书,并在各大平台更新微信支付设置,否则会影响正常支付。

0.1.2. API证书:apiclient_cert.p12 的部署

微信支付接口中,涉及资金回滚的接口会使用到API证书,包括退款、撤销接口。商家在申请微信支付成功后,收到的相应邮件后,可以按照指引下载API证书,也可以按照以下路径下载:【账户中心】-【账户设置】-【API安全】。
对证书相关的官方描述详见:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3
在php开发环境下,证书为两个文件:

证书附件 描述 使用场景 备注
apiclient_cert.pem 从apiclient_cert.p12中导出证书部分的文件,为pem格式,请妥善保管不要泄漏和被他人复制 PHP等不能直接使用p12文件,而需要使用pem,为了方便您使用,已为您直接提供 您也可以使用openssl命令来自己导出:openssl pkcs12 -clcerts -nokeys -in apiclient_cert.p12 -out apiclient_cert.pem
apiclient_key.pem 从apiclient_key.pem中导出密钥部分的文件,为pem格式,请妥善保管不要泄漏和被他人复制 PHP等不能直接使用p12文件,而需要使用pem,为了方便您使用,已为您直接提供 您也可以使用openssl命令来自己导出:openssl pkcs12 -nocerts -in apiclient_cert.p12 -out apiclient_key.pem

除php环境外,其他开发环境的证书都是。.p12文件格式:

证书附件 描述 使用场景 备注
apiclient_cert.p12 包含了私钥信息的证书文件,为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份 撤销、退款申请API中调用 windows上可以直接双击导入系统,导入过程中会提示输入证书密码,证书密码默认为您的商户ID(如:10010000)

当我们部署一个web服务时,需将对应的证书文件放置在服务所在服务器的选定目录中(注意确保选定目录有访问权限,避免其他用户随意下载),并将最终证书的储存路径配置到服务的配置文件中。

示例代码 :

0.1.2.0.1. 配置文件: application.yml
…
….:
appid: …
 mchKey: …
 api: https://api.mch.weixin.qq.com/pay/unifiedorder
keyPath: /home/BU/pay/wechat/cert/apiclient_cert.p12 
 notify_url: …
…
…
0.1.2.0.2. 基础配置类
package com.util.wechatPay;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
@Component
public class WechatAccountConfig {
/**
 * 公众账号ID
 */
    @Value("${wxpay.appid}")
    private String mpAppId;

/**
 * 商户号
 */
    @Value("${wxpay.mchid}")
private String mchId;

/**
 * 商户密钥
 */
    @Value("${wxpay.mchKey}")
private String mchKey;

/**
 * 商户证书路径
 */
    @Value("${wxpay.keyPath}")
private String keyPath;

/**
 * 微信支付异步通知地址
 */
    @Value("${wxpay.notify_url}")
private String notifyUrl;
}

此外,在普通的网络环境下,HTTP请求存在DNS劫持、运营商插入广告、数据被窃取,正常数据被修改等安全风险。商户回调接口使用HTTPS协议可以保证数据传输的安全性。所以微信支付建议商户提供给微信支付的各种回调采用HTTPS协议。关于HTTPS服务器相关配置可参见官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=10_4

0.1.3. 支付结果通知回调:notify_url

关于支付结果通知的官方说明可参见:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_7&index=3
简单来说,当交易完成后,支付平台会按照notify_url配置的地址将交易结果以后台的方式发送到服务网站。
服务在Notify_url指定的controller接口中,可以仅接收数据,不做任何业务处理;也可以在controller层方法中自行设置相关业务逻辑或输出日志。
notify_url的配置环节,因微信官方对此没有太详细的说明,因此配置的过程中非常容易出现配置失败、服务不生效的情况。根据以往的经验,总体来说,notify_url的配置需要注意如下事项:

  1. notify_url的协议一定是https,且地址必须为已备案的域名,不支持ip:端口的形式。
  2. notify_url不能有参数,外网可以直接访问,不能有访问控制(如必须要登录才能操作)。
  3. 回调的链接一定是外网能够访问且能接收到POST信息的。
  4. 微信生态回调回来的数据通常为xml格式,类似于:
<xml><appid><![CDATA[wx67c67bf9f6eb0]]></appid>
<attach><![CDATA[weixin]]></attach>
<bank_type><![CDATA[CFT]]></bank_type>
<cash_fee><![CDATA[1]]></cash_fee>
<fee_type><![CDATA[CNY]]></fee_type>
<is_subscribe><![CDATA[N]]></is_subscribe>
<mch_id><![CDATA[1500107992]]></mch_id>
<nonce_str><![CDATA[tgvurs9j5avb4xqb08gk1zfdnrh9s]]></nonce_str>
<openid><![CDATA[ok8E41G5BO-x8t67iAlJ8WxhU]]></openid>
<out_trade_no><![CDATA[10201862911214739644529]]></out_trade_no>
<result_code><![CDATA[SUCCESS]]></result_code>
<return_code><![CDATA[SUCCESS]]></return_code>
<sign><![CDATA[830CFDFC0788CBB7B8EC349E2CEAD87B]]></sign>
<time_end><![CDATA[201806291122805]]></time_end>
<total_fee>1</total_fee>
<trade_type><![CDATA[NATIVE]]></trade_type>
<transaction_id><![CDATA[42000001201806295324555341365]]></transaction_id>
</xml>

因此在解析的过程中,注意信息的正确读取和使用方法。

示例代码:

0.1.3.0.1. 配置文件: application.yml
…
…
wxpay:
 mchid: …
appid: …
 mchKey: …
 api: https://api.mch.weixin.qq.com/pay/unifiedorder
keyPath:…
 notify_url: https://<server_host>/wxpay/notify
…
…
0.1.3.0.2. Controller层
package com.Controller;

import com.ServiceImpl.WXPay_mchAccount_ServiceImpl;
…
…
import java.util.Map;

@Slf4j
@RestController
@RequestMapping(value="/wxpay",produces="application/json;charset=UTF-8")
public class PayController {
…
…
/**
     * @author ShawnNie
     * 2019-11-28 12:20:45
     */
@ApiOperation(value="获取微信支付回调信息(回调服务器资源URL专用,非程序调用接口)", notes="获取微信支付回调信息(回调服务器资源URL专用,非程序调用接口)")
    @RequestMapping(value = "/notify",method = RequestMethod.POST)
public ModelAndView notify(@RequestBody String notifyData) throws Exception {
//方法内部可以添加服务器收到微信支付成功回调之后相关的流程逻辑,如:标记系统订单状态、更新订单产品激活、库存状态等业务逻辑
//示例:
log.info("【异步回调】request={}", notifyData);
PayResponse response = bestPayService.asyncNotify(notifyData);
log.info("【异步回调】response={}", JsonUtil.toJson(response));

wXPay_mchAccount_ServiceImpl.sign_origin_order_onto_payment(response.getAppId(),response.getOrderId(),response.getOrderAmount(),response.getOutTradeNo());

return new ModelAndView("pay/success");
}
}

0.1.4. 微信支付授权目录

在业务中发起支付的页面地址必须在授权目录下,否则调用下单接口时会提示“当前页面的URL未注册”。

经实际测试,授权域名可以按照如下规律来配置:

支付授权目录是支付行为相关页面/api的上一级目录。

如:
在一个应用中:
发起微信支付操作的url为:http://www.a.com/recharge/index
那么这里的授权目录应配置为:http://www.a.com/recharge/

同样,这里的授权目录也要忽略参数(也不能做成带hash路由的的页面,同样,单页应用在这里的配置会非常容易出问题。因此在微信支付生态下,尽量避免使用单页应用开发模式)。

0.2. 微信公众号(服务号)-微信支付的接入(JSAPI)

公众号支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。
微信支付支持在公众平台注册并完成微信认证的服务号,政府或媒体订阅号接入支付功能。公众号接入支付后,可以通过JSAPI支付产品来完成在公众号、朋友圈、聊天窗口等微信内的收款需求。

微信服务号-微信支付接入有两种方式:

第一种方式:在微信服务号后台,开通支付(流程相对复杂,不推荐)。

进入微信公众号平台,从左边的栏目里面选择微信支付,点击进去然后点击申请开通微信支付:

接下来根据页面的引导输入相关信息,等待微信官方审核。

第二种方式:在微信支付商户后台,绑定服务号appid(流程相对简洁快速,推荐)。

具体流程如下:

  1. 打开微信支付商户号
  2. 产品中心-看左边的appid授权管理打开
  3. 新增
  4. 然后去把需要绑定的服务号appid复制进去。

账号关联是一个双向确认的过程,当发起关联申请后,需要被关联的账号的管理员确认才可建立绑定关系,申请单若7天内未被确认,则自动失效,需要重新发起。

待微信服务号与微信支付商户完成绑定之后,即可在相关的服务中添加对应的配置参数:

0.2.0.0.1. 配置文件: application.yml
…
…
wxpay:
mchid: …
appid: wx0fd48b6b2847d9a2
…
  api: https://api.mch.weixin.qq.com/pay/unifiedorder …
…

0.3. 微信支付第三方sdk开发:best_pay_sdk

微信支付官方文档详见:https://pay.weixin.qq.com/wiki/doc/api/index.html

微信官方对支付生态的api文档描述不尽详细,直接用原生的api进行开发,效率低,而且较容易踩坑,花费大量时间进行排查及优化。因此,微信支付的开发比较推荐使用一些主流的第三方sdk进行辅助开发。
在一些主流的第三方开源sdk中,best-pay-sdk相对稳定性及安全性最佳。因此在这里推荐并简单示范相关功能的代码逻辑。
项目详见:https://github.com/Pay-Group/best-pay-sdk 。这个SDK使用PayRequest和PayResponse对请求接口和相应结果做了大量的封装,主要需要动态传入的参数是openid(用户唯一标识)和orderId。相对原生的微信支付api,相关接口代码更加清晰简洁。与此同时,sdk也支持支付宝生态相关的大多数功能,对项目后续支付形式扩展也非常友好。

使用步骤如下:

  1. 首先在项目中引入依赖:

    cn.springboot best-pay-sdk 1.2.0
  2. 在项目配置文件中正常添加微信支付、绑定服务号APPID等信息配置:

0.3.0.0.1. 配置文件: application.yml
…
…  
wxpay:
mchid: 1554087381
appid: wx0fd48b6b2847d9a2
 mchKey: EmotibotXZDADDYDJIWJ39439085fjei
 api: https://api.mch.weixin.qq.com/pay/unifiedorder
 keyPath: /home/BU/pay/wechat/cert/apiclient_cert.p12 
 notify_url: https://botdaddy.emotibot.com/wxpay/notify
…
…
0.3.0.0.2. 基础配置类
package com.util.wechatPay;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
@Component
public class WechatAccountConfig {
/**
 * 公众账号ID
 */
    @Value("${wxpay.appid}")
    private String mpAppId;

/**
 * 商户号
 */
    @Value("${wxpay.mchid}")
private String mchId;

/**
 * 商户密钥
 */
    @Value("${wxpay.mchKey}")
private String mchKey;

/**
 * 商户证书路径
 */
    @Value("${wxpay.keyPath}")
private String keyPath;

/**
 * 微信支付异步通知地址
 */
    @Value("${wxpay.notify_url}")
private String notifyUrl;
}
  1. 微信公众账号支付配置:
0.3.0.0.3. Best-pay-sdk – payconfig类
package com.util.wechatPay;

import com.lly835.bestpay.config.WxPayH5Config;
import com.lly835.bestpay.service.impl.BestPayServiceImpl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class PayConfig {

@Autowired
private WechatAccountConfig accountConfig;

@Bean
public WxPayH5Config wxPayH5Config() {
WxPayH5Config wxPayH5Config = new WxPayH5Config();
wxPayH5Config.setAppId(accountConfig.getMpAppId());
wxPayH5Config.setMchId(accountConfig.getMchId());
wxPayH5Config.setMchKey(accountConfig.getMchKey());
wxPayH5Config.setKeyPath(accountConfig.getKeyPath());
wxPayH5Config.setNotifyUrl(accountConfig.getNotifyUrl());
return wxPayH5Config;
}

@Bean
public BestPayServiceImpl bestPayService(WxPayH5Config wxPayH5Config) {
BestPayServiceImpl bestPayService = new BestPayServiceImpl();
bestPayService.setWxPayH5Config(wxPayH5Config);
return bestPayService;
}
}
  1. 编写controller层方法:发起支付、接收回调等:

    package com.Controller;

    import com.ServiceImpl.WXPay_mchAccount_ServiceImpl;
    import com.lly835.bestpay.enums.BestPayTypeEnum;
    import com.lly835.bestpay.model.PayRequest;
    import com.lly835.bestpay.model.PayResponse;
    import com.lly835.bestpay.service.impl.BestPayServiceImpl;
    import com.lly835.bestpay.utils.JsonUtil;
    import io.swagger.annotations.ApiImplicitParam;
    import io.swagger.annotations.ApiImplicitParams;
    import io.swagger.annotations.ApiOperation;
    import io.swagger.annotations.ApiParam;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.servlet.ModelAndView;
    import java.util.Map;

    @Slf4j
    @RestController
    @RequestMapping(value=”/wxpay”,produces=”application/json;charset=UTF-8”)
    public class PayController {
    @Autowired
    private BestPayServiceImpl bestPayService;
    @Autowired
    private WXPay_mchAccount_ServiceImpl wXPay_mchAccount_ServiceImpl;

    /**

      * @author ShawnNie
      * 2019-11-28 12:20:45
      */
    

    @ApiOperation(value=”发起微信支付”, notes=”发起微信支付”)

     @RequestMapping(value = "/pay",method = {RequestMethod.POST})
     @ApiImplicitParams({
    

    // @ApiImplicitParam(name = “wxpay_appId”, value = “公众账号APPID”, required = true, dataType = “String”, paramType = “query”),

         @ApiImplicitParam(name = "wxVisitor_openid", value = "用户微信openid", required = true, dataType = "String", paramType = "query"),
         @ApiImplicitParam(name = "order_code", value = "系统订单编号", required = true, dataType = "String", paramType = "query"),
         @ApiImplicitParam(name = "order_price_amount", value = "系统订单金额(单位:¥)", required = true, dataType = "double", paramType = "query"),
         @ApiImplicitParam(name = "map", value = "request附加请求参数(非必填项,可为空)", required = false, paramType = "query")
     })
    

    public PayResponse pay(
    // @ApiParam(name=”wxpay_appId”,type=”String”) String wxpay_appId,

     @ApiParam(name="wxVisitor_openid",type="String") String wxVisitor_openid,
     @ApiParam(name="order_code",type="String") String order_code,
     @ApiParam(name="order_price_amount",type="double") double order_price_amount,
     @ApiParam(name="map")Map<String, Object> map
    

    ) {
    PayRequest request = new PayRequest();
    //支付请求参数
    request.setPayTypeEnum(BestPayTypeEnum.WXPAY_H5);
    request.setOrderId(order_code);
    request.setOrderAmount(order_price_amount);
    request.setOrderName(order_code);
    request.setOpenid(wxVisitor_openid);
    log.info(“【发起支付】request={}”, JsonUtil.toJson(request));
    PayResponse payResponse = bestPayService.pay(request);
    log.info(“【支付响应】response={}”, JsonUtil.toJson(payResponse));
    map.put(“payResponse”, payResponse);
    return payResponse;
    }

    /**

      * @author ShawnNie
      * 2019-11-28 12:20:45
      */
    

    @ApiOperation(value=”获取微信支付回调信息(回调服务器资源URL专用,非程序调用接口)”, notes=”获取微信支付回调信息(回调服务器资源URL专用,非程序调用接口)”)

     @RequestMapping(value = "/notify",method = RequestMethod.POST)
    

    public ModelAndView notify(@RequestBody String notifyData) throws Exception {
    log.info(“【异步回调】request={}”, notifyData);
    PayResponse response = bestPayService.asyncNotify(notifyData);
    log.info(“【异步回调】response={}”, JsonUtil.toJson(response));

    wXPay_mchAccount_ServiceImpl.sign_origin_order_onto_payment(response.getAppId(),response.getOrderId(),response.getOrderAmount(),response.getOutTradeNo());

    return new ModelAndView(“pay/success”);
    }
    }

  1. 配置response模板(为适配微信生态数据格式):
0.3.0.0.4. /resources/templates/pay/create.ftl
<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" ) {
alert('支付成功');
}else if(res.err_msg == "get_brand_wcpay_request:cancel") {
alert('支付过程中用户取消');
}else if(res.err_msg == "get_brand_wcpay_request:fail") {
alert('支付失败');
}else {
alert('未知异常');
}
}
);
}
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>
0.3.0.0.5. /resources/templates/pay/success.ftl
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>