附:代码仓库地址
微信支付官方文档地址:微信支付官方文档
首先,使用的最知名语言-- Java,然后我们说一下在支付上微信提供的API
微信支付API:
统一下单 https://api.mch.weixin.qq.com/pay/unifiedorder 不需要使用证书
查询订单 https://api.mch.weixin.qq.com/pay/orderquery 不需要使用证书
关闭订单 https://api.mch.weixin.qq.com/pay/closeorder 不需要使用证书
申请退款 https://api.mch.weixin.qq.com/secapi/pay/refund 需要双向证书
查询退款 https://api.mch.weixin.qq.com/pay/refundquery 不需要使用证书
下载对账单 https://api.mch.weixin.qq.com/pay/downloadbill 不需要使用证书
下载资金账单 https://api.mch.weixin.qq.com/pay/downloadfundflow 需要双向证书
交付结果通知 自定义 不需要使用证书
交易保障 https://api.mch.weixin.qq.com/payitil/report 不需要使用证书
退款结果通知 自定义 不需要使用证书
拉取订单评价数据 https://api.mch.weixin.qq.com/billcommentsp/batchquerycomment 需要双向证书
以下只说微信公众号中的微信支付-- jsapi,项目的开发使用前后端分离模式
前端使用vue,前端在微信支付使用JS-SDK,
wx.chooseWXPay({
timestamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: '', // 支付签名随机串,不长于 32 位
package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: '', // 支付签名
success: function (res) {
// 支付成功后的回调函数
}
});
后端使用JSAPI,在后端我们可以使用微信JSAPI所提供的sdk,
虽然是微信支付开发文档所提供的SDK(即官方sdk),但是这个SDK中存在很多坑,在这里讲一下我做公众号支付引入微信sdk所踩过的坑
公众号支付,在公众号内对订单进行支付需要wx.chooseWXPay中参数的补充完全,参数的不全需要后端提供接口,则需要在后端写统一下单接口,后端进行与微信服务器进行交互
首先说一下sdk中统一下单接口中的错误,没有进行二次加密将数据返回给前端wx.chooseWXPay(),
附正确代码:
public Map<String, String> unifiedOrder(String ip, Long consumeId) throws Exception {
Map<String, String> orderMap = new HashMap<>();
MyConfig config = new MyConfig();
WxPay wxpay = new WxPay(config);
MyConfig myConfig = new MyConfig();
Map<String, String> map = new HashMap<>();
map.put("out_trade_no", String.valueOf(consumeId));
map.put("appid", myConfig.getAppID());
map.put("mch_id", myConfig.getMchID());
map.put("nonce_str", WxPayUtil.generateNonceStr());
map.put("openid", UserManager.getOpenId());
map.put("body", "账单");
map.put("spbill_create_ip", ip);
String b = consumeRecord.getAmount().multiply(BigDecimal.valueOf(100)).toBigInteger().toString();
map.put("total_fee", "1");
map.put("notify_url", payUrl);
map.put("trade_type", "JSAPI");
//生成签名
String sign = WxPayUtil.generateSignature(map, config.getKey(), PaymentConstants.MD5);
map.put("sign", sign);
Map<String, String> resp = wxpay.unifiedOrder(map);
//判断下单是否成功
String return_code = resp.get("return_code");
String result_code = resp.get("result_code");
if (PaymentConstants.RETURN_SUCCESS.equals(return_code) && PaymentConstants.RETURN_SUCCESS.equals(result_code)) {
//下单成功后进行二次加密(二次加密需要带上时间戳),将结果返回
String prepayId = "prepay_id=" + resp.get("prepay_id");
orderMap.put("appId", myConfig.getAppID());
orderMap.put("nonceStr", resp.get("nonce_str"));
orderMap.put("package", prepayId);
orderMap.put("signType", PaymentConstants.MD5);
orderMap.put("timeStamp", String.valueOf(WxPayUtil.getCurrentTimestamp()));
orderMap.put("sign", WxPayUtil.generateSignature(orderMap, config.getKey(), PaymentConstants.MD5));
return orderMap;
} else {
return resp;
}
}
以上统一下单方法中,需要向微信服务器发送请求,map中的key一定按照微信支付开发文档中相关功能提供的字段去写
重要的事情讲三遍 统一下单一定要进行二次加密!!! 统一下单一定要进行二次加密!!! 统一下单一定要进行二次加密!!!
在使用java SDK的时候其中要注意的地方,在使用沙箱测试时,尤其注意SDK中
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.wxPayRequest = new WXPayRequest(config);
}
以上方法中是否开启沙箱一定要判断清楚,开启沙箱使用的加密,sdk中默认使用的加密方式是MD5,而沙箱使用的加密方式使用的是HMACSHA256。
关于微信支付沙箱测试的使用在SDK中没有提供key的生成工具类,传输数据的sign需要多传输字段进行HMACSHA256进行加密方法中有提到,以下是向微信支付服务器请求到的key
public static String getSignKey() {
try {
String uri = "https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey";//**获取仿真测试环境验签秘钥API**
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);//媒体类型`application/xml`
Map<String, String> map = new HashMap<String, String>();
map.put("mch_id", PaymentConstants.MCH_ID);//商户号
map.put("nonce_str", WxPayUtil.generateNonceStr());//随机字符串**
map.put("sign", WxPayUtil.generateSignature(map, PaymentConstants.KEY, PaymentConstants.MD5));//用生产环境的KEY对mch\_id、nonce\_str 请求参数签名**
String params = mapToXml(map);
String responseXML = OkHttpUtil.getInstance().xmlPost(uri, params);
Map<String, String> xmlToMap = WxPayUtil.xmlToMap(responseXML);
String sandbox_signkey = xmlToMap.get("sandbox_signkey");
return sandbox_signkey;//这个就是沙箱签名key
} catch (URISyntaxException e) {
throw new BusinessException(e.getMessage());
} catch (Exception e) {
throw new BusinessException(e.getMessage());
}
}
公众号支付方式 通过统一下单接口和js-sdk wx.chooseWXPay()可以唤起微信支付界面
以上是微信公众号支付怎么唤醒微信支付,接下来是微信支付成功通知的回调
上面图片中需要注意的事项尤其重要,请多读几遍,弄清楚处理步骤,涉及money请慎重在慎重,小伙伴!!!
接下来说一下我是怎么处理的回调通知,首先在统一下单中配置notify_url,支付结果通知接口地址,一定要是一个共网下可以访问的接口页面。我的回调地址为:
http://zzxhub.free.idcfengye.com/wxpay/JSPayNotify
回调接口代码:
@RestController
@RequestMapping("/wxpay")
public class WxPayController {
@Autowired
WxPayConfigService wxPayConfigService;
@PostMapping("/JSPayNotify")
@ApiOperation("完成下单后异步回调")
public String JSPayNotify() throws Exception {
return wxPayConfigService.jsPayNotify(wxPayConfigService.getNotifyStr(request));
}
}
/**
* 下单成功
*
* @return
* @throws Exception
*/
@Override
public String jsPayNotify(String sb) throws Exception {
Map<String, String> payNotifyMap = WxPayUtil.xmlToMap(sb);
System.out.println(payNotifyMap);
if (payNotifyMap.isEmpty()) {
return fail();
}
//先校验签名是否正确若正确进行后续处理
if (WxPayUtil.isSignatureValid(payNotifyMap, PaymentConstants.KEY)) {
String outTradeNo = payNotifyMap.get("out_trade_no");
if (tradeService.hasProcessed(outTradeNo)) {
log.info("该账单已经支付");
return success();
}
if (PaymentConstants.RETURN_SUCCESS.equals(payNotifyMap.get("return_code"))) {
if (PaymentConstants.RETURN_SUCCESS.equals(payNotifyMap.get("result_code"))) {
//下单成功
//签名正确
String transactionId = payNotifyMap.get("transaction_id");
if (transactionId != null) {
//支付成功
//查询订单状态
Map<String, String> orderMap = orderQuery(transactionId);
String tradeState = orderMap.get("trade_state");
if (PaymentConstants.SUCCESS.equals(tradeState)) {
//支付成功完成自身业务
Integer payStatus = ConsumerConstants.PAYMENT;
log.info("支付成功,开始更新自身业务");
updateConsumeAndTrade(transactionId, orderMap, payStatus);
}
}
} else {
log.info("支付失败");
return fail();
}
}
return success();
} else {
log.info("签名签名校验失败");
return fail();
}
}
String fail() {
return " ";
}
String success() {
return " ";
}
以上是支付成功结果通知回调接口,一定要注意无论是对支付通知结果回调的处理成功还是失败都要发送给微信一个xml形式的通知,再有就是要注意在该接口中一定要在每一步做处理做日志输出方便查看出错位置,还有记得加事务锁(分布式锁)。
以上是微信公众号支付的两个接口。
接下来是微信退款了,微信退款也跟支付的形式类似,同样有微信退款接口,和退款结果通知接口。
首先是微信退款接口
/**
* 申请退款 请求微信支付退款api需要证书
* @param refundId
* @return
* @throws Exception
*/
@Override
public Map<String, String> refund(Long refundId) throws Exception {
Trade trade = tradeService.findTradeById(refundId);
ConsumeRecord consumeRecord = consumeRecordService.findConsumeRecordById(trade.getConsumeId(), ConsumerConstants.RETURNING_MONEY);
BigDecimal amount = trade.getAmount().abs();
MyConfig config = new MyConfig();
WxPay wxpay = new WxPay(config);
Map<String, String> map = new HashMap<>();
map.put("appid", config.getAppID());
map.put("mch_id", config.getMchID());
map.put("nonce_str", WxPayUtil.generateNonceStr());
map.put("out_trade_no", String.valueOf(trade.getConsumeId()));
map.put("out_refund_no", String.valueOf(trade.getId()));
map.put("total_fee", consumeRecord.getChargeAmount().multiply(BigDecimal.valueOf(100)).toBigInteger().toString());
map.put("refund_fee", amount.multiply(BigDecimal.valueOf(100)).toBigInteger().toString());
//设置退款通知回调地址,也是公网可以访问的一个接口地址
map.put("notify_url", refundUrl);
map.put("sign", WxPayUtil.generateSignature(map, config.getKey(), PaymentConstants.MD5));
Map<String, String> refundMap = wxpay.refund(map);
return refundMap;
}
@PostMapping("/refundNotify")
@ApiOperation("退款回调")
public Result JSRefundNotify() throws Exception {
return Result.success(wxPayConfigService.refundNotify(wxPayConfigService.getNotifyStr(request)));
}
@Override
public String refundNotify(String notify) throws Exception {
Map<String, String> map = WxPayUtil.xmlToMap(notify);
Map<String, String> refundNotifyMap = WxPayUtil.xmlToMap(AesUtil.decryptData(map.get("req_info")));
if (refundNotifyMap == null) {
return fail();
}
String refundNo = refundNotifyMap.get("out_refund_no");
if (tradeService.hasRefunded(refundNo)) {
return success();
}
if (PaymentConstants.RETURN_SUCCESS.equals(map.get("return_code"))) {
if (PaymentConstants.RETURN_SUCCESS.equals(refundNotifyMap.get("refund_status"))) {
log.info("开始退款业务处理");
String refundId = refundNotifyMap.get("refund_id");
//退款成功
if (refundId != null) {
//成功
//查询订单状态
Map<String, String> orderMap = refundQuery(refundId);
String refundState = refundNotifyMap.get("refund_status");
if (PaymentConstants.SUCCESS.equals(refundState)) {
//退款成功完成自身业务
Integer payStatus = ConsumerConstants.RETURNED_MONEY;
log.info("退款业务处理开始");
updateConsumeAndTrade(refundId, refundNotifyMap, payStatus);
log.info("退款业务处理完毕");
return success();
}
return fail();
}
return fail();
} else {
return fail();
}
} else {
return fail();
}
}
微信退款结果通知数据样例:
<xml>
<return_code>SUCCESSreturn_code>
<appid>appid>
<mch_id>mch_id>
<nonce_str>nonce_str>
<req_info>req_info>
xml>
<root>
<out_refund_no>out_refund_no>
<out_trade_no>out_trade_no>
<refund_account>refund_account>
<refund_fee>refund_fee>
<refund_id>refund_id>
<refund_recv_accout>refund_recv_accout>
<refund_request_source>refund_request_source>
<refund_status>refund_status>
<settlement_refund_fee>settlement_refund_fee>
<settlement_total_fee>settlement_total_fee>
<success_time>success_time>
<total_fee>total_fee>
<transaction_id>transaction_id>
root>
其中要对req_info标签下的数据进行解密处理,其中解密的步骤为:
解密方法如下:
/**
* 微信退款通知信息解密
* @author zzx
* @email [email protected]
* @Description Date 2019/8/13 10:40
*/
public class AesUtil {
/**
* 加解密算法/工作模式/填充方式
*/
private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
/**
* 密钥算法
*/
private static final String ALGORITHM = "AES";
/**
* AES解密
*
* @param base64Data 64
* @return str
* @throws Exception e
*/
public static String decryptData(String base64Data) throws Exception {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
final String keyMd5String = DigestUtils.md5Hex(PaymentConstants.KEY).toLowerCase();
SecretKeySpec key = new SecretKeySpec(keyMd5String.getBytes(StandardCharsets.UTF_8), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
cipher.init(Cipher.DECRYPT_MODE, key);
return new String(cipher.doFinal(org.apache.commons.codec.binary.Base64.decodeBase64(base64Data)),
StandardCharsets.UTF_8);
}
}
以上是我关于对微信公众号支付所踩过坑的一个总结,码农小伙伴共同进步,少踩些坑。