需求
想在微信浏览器里面实现微信支付. 查看微信支付文档, 发现要用微信JSAPI公众号支付, 微信H5支付是不能实现的.
自己在实现的时候踩了许多坑, 花费了很多时间. 特此记录, 希望能帮助到看到该文章的开发者.
开发环境
后端SpringBoot, 前端VUE
准备工作
1.首先我们要在微信公众号平台和微信商户平台 注册账号
2.准备JSAPI支付, 和调用微信统一下单的时候, 所需要的信息.
appid: 这个appid是公众号的appId.
重要的一步. 这里appid如果想使用, 必需要在微信商户号那里关联并授权. 可根据 查看指引 这一步如何操作
appsecret: 开发者密码. 可在微信公众号平台, 自己设置. 切记,设置后保存一下, 因为没有地方可以回显.
mer_id: 微信商户号id
key: 微信商户号API秘钥. 配置完之后也需要保存, 因为没有地方可以回显
body: 订单备注
nonce_str: 即用来标识一笔单, 是个随机数, 可自行实现.
openid: 这个重中之重, 需要前后端一起配合, 后面讲解如何获取.
out_trade_no: 订单编号
spbill_create_ip: ip地址,可以获取当前请求的ip地址, 实现方式有很多, 可自行百度实现.
total_fee: 支付金额, 需要注意这个金额的单位是分, 所以在使用的时候, 要自己订单金额 *100, 可能有时候失败, 也是因为这个原因.
sign_type: 加签方式, 也就是生成 sign 的方式, sign后面会提到, 这里默认是MD5, 建议也写上MD5, 方便调起JSAPI支付的时候, 做统一.
trade_type: 调用的支付方式, 因为这里是JSAPI支付, 所以值为 JSAPI
notify_url: 微信支付的回调, 用于支付成功后处理的业务逻辑. 这个地址必须是外网可以访问的地址, 如果支付成功没有进入回调, 可能就是这里的原因.
sign: 签名, 这个需要程序自己生成, 是根据上面的信息生成, 具体方式可看下面的代码.
其中openid还没有获取到, 最费劲的也就是它了. 下面给出获取openid的步骤, 需要前后端一起配合. 具体实现流程, 可根据自身情况.
获取openid
- 用户同意授权, 获取code, 这里获取code的方式, 是我们给前端实现了.
// appid: 需要把我们的appid给前端
// redirect_uri: 回调地址, 前端自己填写, 用来获取code的
https://open.weixin.qq.com/connect/oauth2/authorize?appid=appid&redirect_uri=redirect_uri&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
- 根据code获取openid
有一点需要注意. 这里的code有效时间是5分钟, 且只能用一次, 如果获取openid失败了, 看一下这个code是不是失效了,或者重复使用了, 我们之前前端获取openid失败, 就是因为没有刷新, code重复使用了.
@ApiOperation("根据code获取openId")
@GetMapping("openid")
public Result getOpenIdByCode(@RequestParam String code){
log.info("根据code:{}获取openId", code);
String getopenid_url = "https://api.weixin.qq.com/sns/oauth2/access_token";
String param= "appid="+appid+"&secret="+secret+"&code="+code+"&grant_type=authorization_code";
String openIdStr = HttpUtils.sendGet(getopenid_url, param);
JSONObject json = JSONObject.parseObject(openIdStr);// 转成Json格式
log.info("查询结果: "+json.toString());
String openId = json.getString("openid");// 获取openId
return Result.success(ResultEnum.SELECT_SUCCESS, openId);
}
目前为止, 我们已经获取到了微信JSAPI支付所需要的全部数据, 如果您能跟着实现到了这里, 就已经成功了百分之八十.
下面开始代码实现
- 后端接口准备. 编写支付接口中的JSAPI. 这里主要是为了给前端调起JSAPI支付所需要的数据. 也就是后面返回的这几个值
appId: 也就是这里一直用的appid
timeStamp: 注意,这里是时间戳, 我们之前调起失败, 也是因为这里没用时间错
nonceStr: 随机字符
signType: 签名方式, 和我们调用统一下单生成的签名方式一样. 也要设置为MD5
package: 包名. 注意: 不能写 package, 因为是关键字
paySign: 签名
// 获取当前请求的ip地址
String requestIp = CommonUtils.getIpAddr(request);
// 拼接统一下单地址参数
SortedMap<String,String> params = new TreeMap<>();
params.put("appid", appid);
params.put("body", remark);
params.put("mch_id", merId);
params.put("nonce_str", CommonUtils.generateUUID());
params.put("openid", openId);
params.put("out_trade_no", out_trade_no);//订单号
params.put("spbill_create_ip", requestIp);
params.put("total_fee","100");
params.put("sign_type", "MD5");
params.put("trade_type", "JSAPI");
params.put("notify_url", weChatConfig.notify_url);
String sign = WXPayUtil.createSign(params, weChatConfig.key);
// 生成签名之后, 注意一定要存进去.
params.put("sign", sign);
String payXml;
try {
// 微信支付要求这里必须是一个xml
payXml = WXPayUtil.mapToXml(params);
log.info("payXml: "+payXml);
// 调用微信的统一下单接口
String orderStr = HttpUtils.doPost("https://api.mch.weixin.qq.com/pay/unifiedorder",payXml,4000);
log.info("统一下单返回结果: "+orderStr);
if(StringUtils.isEmpty(orderStr)){
log.error("微信支付失败.原因: 调用微信统一下单接口失败");
throw new TPlusException(ErrorEnum.WECHAT_PAY_ERROR);
}
// 预支付id
String prepay_id = "";
if (orderStr.indexOf("SUCCESS") != -1) {
// 这个工具类可以用微信官方案例中的
Map<String, String> map = WXPayUtil.xmlToMap(orderStr);
prepay_id = map.get("prepay_id");
}
/** 返回给前端所需要的数据 */
SortedMap<String,String> payMap = new TreeMap<>();
payMap.put("appId", appid);
payMap.put("timeStamp", String.valueOf(new Date().getTime()/1000));
payMap.put("nonceStr", CommonUtils.generateUUID());
payMap.put("signType", "MD5");
payMap.put("package", "prepay_id="+prepay_id);
String paySign = WXPayUtil.createSign(payMap, key);
payMap.put("paySign", paySign);
return payMap;
} catch (Exception e) {
log.error("微信支付失败.原因: map参数转xml失败({})", e.getMessage());
throw new TPlusException(ErrorEnum.WECHAT_PAY_ERROR);
}
- 得到需要的数据之后, 前端来说就很简单了, 直接可以根据返回的数据调起微信支付.
也可参考: JSAPI调起支付文档
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":appId, //公众号ID,由商户传入
"timeStamp":timeStamp, //时间戳,自1970年以来的秒数
"nonceStr":nonceStr, //随机串
"package":package,
"signType":signType, //微信签名方式:
"paySign":paySign //微信签名
},
function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ){
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
}
});
}
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();
}
最后一个需要注意的问题是, 前端在调起微信JSAPI支付的时候, 通过本地是无法调用成功的, 必须发布到线上, 而且要在微信商户平台, 配置线上地址的授权域名.
特别注意: 该域名必须和前端访问的域名一致, 不可以配置主域名. 我们一直无法调起支付, 其中有一个坑也是这个原因.
上面Java调用统一下单时所需要的工具类
- CommonUtils
public class CommonUtils {
/**
* 生成uuid,即用来标识一笔单,也用做 微信支付的nonce_str
* @return
*/
public static String generateUUID(){
String uuid = UUID.randomUUID().toString().
replaceAll("-","").substring(0,32);
return uuid;
}
/**
* 获取用户请求ip
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
//根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
//对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
return ipAddress;
}
到此为止, 就已经完成了微信JSAPI支付, 以上纯手写, 如有错误的地方, 欢迎指教. 谢谢!