在电商网站开发中,我们必不可少的功能环节就是“支付”了,我们可以根据公司或者自己的开发需求,选择不一样的支付方式,比如微信支付,支付宝支付,银联支付,以及跨境支付如PayPal等等的众多方式供你选择,今天,我将和大家聊聊微信支付之二微信扫码Native支付与微信JSAPI支付。
根据微信官方提供的文档,我们来进行梳理。首先我们访问:微信支付-开发文档
进入如下界面:
我们首先会调用微信提供的API得到二维码链接,这个链接可以通过前端qrcode.js将链接转换为二维码,也可以在后台通过谷歌的ZXing工具生成二维码。详细介绍如下:
首先,你需要在微信支付的商户平台配置扫码支付的回调域名,具体位置为:商户平台–>产品中心–>开发配置。
当然,你也可以不用配置扫码支付的回调地址,而是在调用微信支付的统一下单API接口中传递参数notify_url。我选的是这种方式,可以将回调配置在yml文件中,自由定义,不需要关心商户平台的回调地址。
最后,还需要选择一种模式,我采用的是模式二,你也可以根据自己的需求,选择你认为合适的开发模式。如下图模式二说明了接下来我们编码的流程:
(1)商户后台系统根据用户选购的商品生成订单。
用户在商城平台提交订单,我们会生成初始订单。
商城平台生成订单之后,接着调用微信支付。进行第二,第三步骤,如下:
(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;
这一步骤,有一个关键的地方是生成商户单号。如下是我生成商户单号的方式。
生成商户单号
@Override
@Transactional(rollbackFor = Exception.class)
public String generateCode(KeSequence.Tag tag, String userMobile) {
Long sequence = this.findSequence(Long.parseLong(tag.getCode()+""));
this.updateSequence(Long.parseLong(tag.getCode()+""),sequence);
return DateWithRandomUtils.numsToRandom()+ sequence+ ((userMobile.length()>4)?(userMobile.substring(userMobile.length()-4)):userMobile);
}
调用工具方法
/**
* 将生成的时间+num位随机数,打乱
* @return
*/
public static String numsToRandom(){
String randomNO = randomNO(4);
char[] chars = randomNO.toCharArray();
List<String> list = new ArrayList<>();
for (char aChar : chars) {
list.add(aChar+"");
}
Collections.shuffle(list);
StringBuffer sb = new StringBuffer();
list.forEach(ch->{
sb.append(ch);
});
return sb.toString();
}
工具方法细节:
/**
* 唯一编号 = 点前时间年月日时分秒+num位随机数
* @return
*/
public static String randomNO(int num){
String newsNo = timeNoYMDHMS();
long random = getRandom(num);
return newsNo+random;
}
/**
* 获取当前时间 yyyyMMddHHmmss
* @return
*/
public static String timeNoYMDHMS(){
//设置日期格式
SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
// new Date()为获取当前系统时间,也可使用当前时间戳
String newsNo = df.format(new Date());
return newsNo;
}
/**
* 生成固定长度随机码
* @param n 长度
*/
public static long getRandom(long n) {
long min = 1,max = 9;
for (int i = 1; i < n; i++) {
min *= 10;
max *= 10;
}
long rangeLong = (((long) (new Random().nextDouble() * (max - min)))) + min ;
return rangeLong;
}
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
生成商户单号,接着可以通过微信提供的SDK,调用统一下单的方法了,我的做法如下:
核心代码:
/**
* 微信支付
* @param keOrder
* @return
*/
private ResultMsg WxUnifiedorder(KeOrder keOrder,String bizPayNo, Integer tradeType, String openid) throws Exception {
//设置需要的参数
WxParamQuery wxParamQuery = new WxParamQuery();
//是
wxParamQuery.setOut_trade_no(bizPayNo);
//以分为单位
wxParamQuery.setTotal_fee(keOrder.getActualTotal().multiply(BigDecimal.valueOf(100)).longValue()+"");
String body = "";
if(!StringUtils.isEmpty(keOrder.getProdName())){
body = keOrder.getProdName().length()>15 ? keOrder.getProdName().substring(0,15)+"...":keOrder.getProdName();
}
wxParamQuery.setBody(body);
wxParamQuery.setReceipt("N");
// 注意:此处配置交易方式, JSPAI, NATIVE...
wxParamQuery.setTrade_type(WxParamQuery.getTradeTypeMap(tradeType));
//否
wxParamQuery.setAttach(keOrder.getOrderNo());
wxParamQuery.setProduct_id("");
wxParamQuery.setOpenid(openid);
wxParamQuery.setDetail("");
wxParamQuery.setDevice_info("WEB");
wxParamQuery.setUserId(keOrder.getUserId());
return wxPayService.unifiedorder(wxParamQuery);
}
调用统一下单API:
/**
* 微信支付-统一下单
* @param wxParamQuery
* @return
*/
@Override
@Transactional
public ResultMsg unifiedorder(WxParamQuery wxParamQuery) throws Exception {
Map<String,String> data = new ConcurrentHashMap();
data.put("device_info",wxParamQuery.getDevice_info());
data.put("body",wxParamQuery.getBody());
data.put("detail",wxParamQuery.getDetail());
data.put("attach",wxParamQuery.getAttach());
data.put("out_trade_no",wxParamQuery.getOut_trade_no());
data.put("total_fee",wxParamQuery.getTotal_fee());
data.put("trade_type",wxParamQuery.getTrade_type());
data.put("product_id",wxParamQuery.getProduct_id());
data.put("openid",wxParamQuery.getOpenid());
data.put("receipt",wxParamQuery.getReceipt());
data.put("spbill_create_ip", spbill_create_ip);
if("JSAPI".equalsIgnoreCase(wxParamQuery.getTrade_type())){
if(StringUtils.isEmpty(wxParamQuery.getOpenid())){
return ResultMsg.fail("缺少参数openid",null);
}
}
try {
WXPayConfig payConfig = new PayConfig(appID,mchID,key,domain,primaryDomain, true);
WXPay wxPay = new WXPay(payConfig,notify_url,true,false);
data = wxPay.fillRequestData(data);
//微信提供的统一下单方法一:
Map<String,String> resMap = wxPay.unifiedOrder(data);
//主动调微信统一下单方法二
/*
String xml = WXPayUtil.mapToXml(data);
String res = HttpHelper.post(pay_url, xml, "text/plain;charset=UTF-8");
Map resMap = WXPayUtil.xmlToMap(res);
*/
if("SUCCESS".equalsIgnoreCase(resMap.get("return_code"))){
if("SUCCESS".equalsIgnoreCase(resMap.get("result_code"))){
// 获取二维码链接
String code_url = resMap.get("code_url");
String prepay_id = resMap.get("prepay_id");
Map<String, String> payParam = wxUtils.getJsapiPayParam(prepay_id);
if(wxParamQuery.getTrade_type().equals(WxParamQuery.TradeType.NATIVE.name())){
return ResultMsg.suc(code_url);
}else{
return ResultMsg.suc(payParam);
}
}
}
String return_msg = resMap.get("return_msg");
return ResultMsg.fail(return_msg,return_msg);
} catch (Exception e) {
e.printStackTrace();
return ResultMsg.fail(e.getMessage(),e.getMessage());
}
}
调用成功之后,微信会返回code_url, 二维码链接,至此,我们可以将二维码链接转换为二维码图片,即可。
当用户扫码支付完成之后,会进入回调,我们可以在回调中修改订单的状态或者其他必要的操作。如下,是我的回调代码。
核心代码:
@RequestMapping(value = "/notifyUrl")
public void notifyUrl(HttpServletRequest request, HttpServletResponse response) throws Exception {
String resXml = "";
try {
log.info("微信notifyUrl回调方法,开始进入。。。");
//读取参数
StringBuffer sb = new StringBuffer();
InputStream inputStream = request.getInputStream();
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String str = null ;
while ((str = in.readLine()) != null){
sb.append(str);
}
in.close();
inputStream.close();
//解析xml成map
Map<String, String> m = WXPayUtil.xmlToMap(sb.toString());
//过滤空 设置 TreeMap
SortedMap<String,String> packageParams = new TreeMap<>();
Iterator<String> it = m.keySet().iterator();
while (it.hasNext()) {
String parameter = it.next();
String parameterValue = m.get(parameter);
String v = "";
if(null != parameterValue) {
v = parameterValue.trim();
}
packageParams.put(parameter, v);
}
log.info("微信支付返回回来的参数:"+packageParams);
try {
// 支付日志入库
iKePayLogService.saveWxPayLog(packageParams);
}catch (Exception e){
e.printStackTrace();
}
//判断签名是否正确
if(WXPayUtil.isSignatureValid(m, key, WXPayConstants.SignType.HMACSHA256)) {
//------------------------------
//处理业务开始
//------------------------------
if("SUCCESS".equalsIgnoreCase( packageParams.get("return_code")) &&
"SUCCESS".equals(packageParams.get("result_code"))){
// 这里是支付成功
//执行自己的业务逻辑开始
String app_id = packageParams.get("appid");
String mch_id = packageParams.get("mch_id");
String openid = packageParams.get("openid");
//是否关注公众号
String is_subscribe = packageParams.get("is_subscribe");
//附加参数【订单号】
String attach = packageParams.get("attach");
//商户订单号
String out_trade_no = packageParams.get("out_trade_no");
//付款金额【以分为单位】
String total_fee = packageParams.get("total_fee");
//微信生成的交易订单号
String transaction_id = packageParams.get("transaction_id");
//支付完成时间
String time_end= packageParams.get("time_end");
log.info("app_id:"+app_id);
log.info("mch_id:"+mch_id);
log.info("openid:"+openid);
log.info("is_subscribe:"+is_subscribe);
log.info("out_trade_no:"+out_trade_no);
log.info("total_fee:"+total_fee);
log.info("额外参数_attach:"+attach);
log.info("time_end:"+time_end);
//执行自己的业务逻辑开始
log.info("支付成功。。。执行自己的业务逻辑开始");
OrderPayDTO orderPayDTO = new OrderPayDTO();
orderPayDTO.setTransaction_id(transaction_id);
orderPayDTO.setOut_trade_no(out_trade_no);
orderPayDTO.setMch_id(mch_id);
orderPayDTO.setApp_id(app_id);
orderPayDTO.setAttach(attach);
orderPayDTO.setTime_end(LocalDateTime.now());
orderPayDTO.setOpenid(openid);
orderPayDTO.setIs_subscribe(is_subscribe);
orderPayDTO.setTotal_fee(BigDecimal.valueOf(Double.parseDouble(total_fee)));
orderPayDTO.setPayType(KeOrder.orderPayType.WECHAT_PAY.getCode());
//调用业务处理方法
iKeOrderService.orderPaySuc(orderPayDTO);
//执行自己的业务逻辑结束
//通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
resXml = notifyToWx("SUCCESS","OK");
} else {
log.info("支付失败,错误信息:" + packageParams.get("err_code"));
resXml = notifyToWx("FAIL",packageParams.get("err_code"));
}
} else{
resXml = notifyToWx("FAIL","签名验证失败");
}
}catch (Exception e){
e.printStackTrace();
resXml = notifyToWx("FAIL","程序异常");
}
//------------------------------
//处理业务完毕
//------------------------------
BufferedOutputStream out = new BufferedOutputStream(
response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
}
public String notifyToWx(String return_code, String return_msg){
return "" + "+return_code+"]]> "
+ "+return_msg+"]]> " + " ";
}
JSAPI支付的后端实现逻辑和Native之后一样,只需要多传入一个必传参数openid,上面已经列举,我就不必再次重复说明了。 接下来,我将说明和Native不同的实现逻辑。
我们开发公众号时,需要调用微信支付,这时候就是需要JSPAPI支付了,JSAPI需要通过网页授权,获取用户的openid,也就是上述步骤中调用微信的统一下单需要的步骤,然后再改变trade_type为JSAPI,如下
其余步骤与Native一致。
JSAPI支付必须要在商户平台配置支付目录,所谓支付目录就是指:前端调起微信支付的当前URL地址。 设置如下:
调起支付,需要配置jsconfig,我用的是vue, 以下我将介绍vue的方式发起支付。
首先前端配置如下:
mounted() {
this.axios
.get(
"/kuais/essleyWeb/essley/wx/getWxConfigParams",
{
params: {
webUrl: this.webUrl
}
}
)
.then(res => {
var data = res.data.data;
console.log(data)
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: data.appId, // 必填,公众号的唯一标识
timestamp: data.timestamp, // 必填,生成签名的时间戳
nonceStr: data.nonceStr, // 必填,生成签名的随机串
signature: data.signature, // 必填,签名,见附录1
jsApiList: ["chooseWXPay"]
});
wx.ready(function() {
wx.checkJsApi({
jsApiList: ["chooseWXPay"],
success: function(res) {
console.log("seccess");
console.log(res);
},
fail: function(res) {
console.log("fail");
console.log(res);
}
});
});
});
},
我们需要在后端,获取token,然后获取jsapi_ticket。 得到初始化参数之后,返回给前端,配置jsconfig。 可以通过微信开发者工具中查看是否配置成功,(有时候你看JS-SDK中提示签名错误,其实也是可以调起微信支付的)。 代码如下:
/**
* 签名算法
签名生成规则如下:参与签名的字段包括noncestr(随机字符串),
有效的jsapi_ticket, timestamp(时间戳),
url(当前网页的URL,不包含#及其后面部分) 。
对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,
使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。
这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。
*/
public Map<String,String> wxConfigParams(String webUrl){
String jsapi_ticket = jsapiTicketFromRedis();
String noncestr = WXPayUtil.generateNonceStr();
String timestamp = WXPayUtil.getCurrentTimestamp()+"";
String url = webUrl;
String[] arr = new String[]{jsapi_ticket,noncestr, timestamp,url};
String string1 = "jsapi_ticket="+jsapi_ticket+"&noncestr="+noncestr+"×tamp="+timestamp+"&url="+url;
log.info("拼接好的string1: "+string1);
String sha1Hex = DigestUtils.sha1Hex(string1.getBytes());
log.info("jsapi_ticket签名: "+sha1Hex);
Map<String,String> resMap = new HashMap<>();
resMap.put("appId", WebAppID);
resMap.put("timestamp", timestamp);
resMap.put("nonceStr", noncestr);
resMap.put("signature", sha1Hex);
return resMap;
}
jsconfig配置成功之后,接下来就可以调用微信统一下单API了, 与Native扫码支付不同的是,它需要返回如下参数:
appId: data.appId,
timestamp: data.timeStamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: data.nonceStr, // 支付签名随机串,不长于 32
package: data.package, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
signType: data.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: data.paySign, // 支付签名
如果后端返回的参数没有问题,这时候,就可以发起微信支付了。输入密码,就可以支付完成。