微信公众号支付开发总结
微信支付文档有多坑,大家都知道,这里就不多说了。
首先说明,本文档编写日期受时间限制,不保证之后一段时间内有效。由于作者水平有限,文字功底较低,有问题可以留评论交流。
目录(可以直接选择自己想看的)
微信支付业务流程
在开发微信支付功能之前,首先还是需要看一下它的业务流程,官方链接:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_4,这里我只是引用它的一张图说明问题
这张图虽然有些问题,不过起码说清楚了开发流程,在开发支付后台时,只需做5、6、10、11即可。退款类似,发起退款时微信会自己发信息告知。
在开始下面几个功能编写之前,我们首先要确保以下几个东西(官网链接:开发步骤):
1. 微信公众号有权限并且开通了微信支付; 2. 在公众号后台和商户后台配置了支付目录和授权域名;
然后根据官方DEMO(下载链接),配置并写代码测试。
首先配置开发环境:
maven:
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
然后配置类MyConfig:
import com.github.wxpay.sdk.WXPayConfig;
import java.io.*;
public class WechatPayConfig implements WXPayConfig {
private byte[] certData;
public WechatPayConfig() throws IOException {
String certPath = System.getProperty("user.dir");//服务器根路径,为"tomcat/bin"
certPath = certPath.substring(0, certPath.lastIndexOf('/')) + "/path/apiclient_cert.p12";
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 AppMessage.getAppId();//固定参数放在特定的类中
}
@Override
public String getMchID() {
return AppMessage.getMchId();
}
@Override
public String getKey() {
return AppMessage.getKey();
}
@Override
public InputStream getCertStream() {
ByteArrayInputStream cretBais = new ByteArrayInputStream(this.certData);
return cretBais;
}
@Override
public int getHttpConnectTimeoutMs() {
return 8000;
}
@Override
public int getHttpReadTimeoutMs() {
return 10000;
}
}
关于appid和mch_id不用多说,key是用来生成签名的,它不是appsecret,请注意不要弄混了。key设置路径:微信商户平台(pay.weixin.qq.com)–>账户设置–>API安全–>**设置(实际目录有些出入,不过差不多)。
到这里,官方DEMO的README.md都给出了详细步骤,下单和申请退款都有介绍,诸位应该可以自己写出来这两个功能了。
微信JS-SDK说明文档
微信JS-SDK官方文档,其实是一个非常有用的说明文档,不要以为有了专门的微信支付开发文档(这里的‘专门’指的是公众号支付)就不用看这个了。我之前很天真,在碰到各种错误解决无果后,才发现这里是最新的JSAPI。
JSSDK使用有五个步骤,必须要按照它的来,公众号支付文档中描写的所有使用js的操作全部用这里的,不能按照原来的文档。绑定域名和引入js文件很简单,这里不做更多的说明。通过config接口注入权限验证配置是必须的,下面是它的原文说明:
所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)。
所以按照这个步骤,前端基本就没有什么问题了。
下面是我的参数类AppMessage:
public class AppMessage {
/*
* 微信要求的开发者账号和密码,以及获取的access_token
* 微信支付,需要商户号,api秘钥,订单结果通知链接,退款结果通知链接,交易类型,和前端发起请求的jsApiTicket
* */
public static String appId = "your_app_id";
public static String appsecret = "your_app_secret";
public static String mchId = "your_mch_id";
public static String key = "your_api_key";
public static String orderNotifyUrl ="your_order_notify_url";
public static String refundNotifyUrl = "your_refund_notify_url";
public static String tradeType = "JSAPI";
public static final String token = "your_token";
public static AccessToken accessToken = null;
public static String jsApiTicket = null;
public static String getJsApiTicket() {
return jsApiTicket;
}
public static void setJsApiTicket(String jsApiTicket) {
AppMessage.jsApiTicket = jsApiTicket;
}
public static String getToken() {
return token;
}
public static AccessToken getAccessToken() {
return accessToken;
}
public static void setAccessToken(AccessToken accessToken) {
AppMessage.accessToken = accessToken;
}
public static String getAppId() {
return appId;
}
public static String getMchId() {
return mchId;
}
public static String getKey() {
return key;
}
public static String getOrderNotifyUrl() {
return orderNotifyUrl;
}
public static String getRefundNotifyUrl() {
return refundNotifyUrl;
}
public static String getTradeType() {
return tradeType;
}
public static String getAppsecret() {
return appsecret;
}
}
统一下单
统一下单官方介绍,下单之后会进入NOTPAY(未支付)状态,具体参考下图:
正常情况下,下单之后订单会进入未支付状态,然后在后台将微信服务器传过来的数据进行组装并签名之后发送给前台。等待用户支付,支付完成后会进入SUCCESS(支付成功),其他三个状态暂时用不到。
下单需要一些参数,这些参数可以在官方文档中查看。下面直接给出代码:
public String order(Integer orderId,String code, HttpSession session, HttpServletRequest request) {
JSONObject jsonObject = new JSONObject();
if (session.getAttribute("userId") == null) {//用户校验
return jsonObject.toJSONString();
}
Object userId = session.getAttribute("userId");
//从数据库中找出该id对应的价格
jsonObject = Jdbc.toJSONObject("select price " +
"from wechat.order where id=? and orderStatus=?;",
orderId, "nopay");
if (jsonObject.get("price") == null) {
jsonObject.put("status", false);
jsonObject.put("msg", "不存在的订单");
} else {
BigDecimal price = (BigDecimal) jsonObject.get("price");
price = price.multiply(new BigDecimal(100));//元 换算成 分
try {
String openId = WechatPayUtil.getOpenId(code);//获取openId,jspAPI下单必须提供这个参数
Map<String, String> responseMap = WechatPayUtil.order(String.valueOf(orderId), String.valueOf(price.intValue()), openId);//下单
//用于获取sign,参数说明请参考官方文档
long timeStamp = System.currentTimeMillis() / 1000;
Map<String, String> map = new HashMap<>();
map.put("appId", responseMap.get("appid"));
map.put("timeStamp", String.valueOf(timeStamp));
map.put("nonceStr", responseMap.get("nonce_str"));
map.put("package", "prepay_id=" + responseMap.get("prepay_id"));
map.put("signType", "MD5");
map.put("paySign", WechatPayUtil.getSign(map,WXPayConstants.SignType.MD5));
map.put("url",request.getHeader("Referer"));
map.put("signature", WechatPayUtil.getSignature(map));
jsonObject.putAll(map);
jsonObject.put("status", true);
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
return jsonObject.toJSONString();
}
WechatPayUtil.order的实现方法可以从官方javaDEMO的README.md中看到,大同小异。
订单结果通知
验证比较简单,官方Demo中给出了实现,看一下就可以写出来了,这里给出我的Spring-MVC处理代码:
/**
* 支付结果通知
*/
@RequestMapping(value = "/orderResponse")
@ResponseBody
public String oderResponse(HttpServletRequest request) throws
Exception {
InputStream is = request.getInputStream();
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int len = 0;
while((len=is.read(bytes))!= -1){
os.write(bytes,0,len);
}
os.close();
is.close();
String requestString = new String(os.toByteArray(),"UTF-8");
Map<String, String> responseMap = WXPayUtil.xmlToMap(requestString);
if (WechatPayUtil.orderResponse(requestString)) {//验证,微信JavaDEMO中给出了这一段实现
//更新数据库
if (responseMap.get("out_trade_no") != null) {
String orderId = responseMap.get("out_trade_no");
if (Jdbc.getJdbcTemplate().update("UPDATE wechat.order SET orderStatus=? WHERE id=?;","paied" ,orderId)) {
responseMap.put("return_code", "SUCCESS");
responseMap.put("return_msg", "OK");
return WXPayUtil.mapToXml(responseMap);
}
}
}
responseMap.put("return_code", "FAIL");
return WXPayUtil.mapToXml(responseMap);
}
WechatPayUtil.orderResponse实现比较简单,这里就不再列出了。
申请退款
//这个比较简单就不贴了,代码太多了。。。
退款结果通知
直接上代码
public static JSONObject refundResponse(String refundData) throws
Exception {
JSONObject jsonObject = new JSONObject();
WechatPayConfig wechatPayConfig = new WechatPayConfig();
WXPay wxPay = new WXPay(wechatPayConfig);
Map<String, String> refundXML = WXPayUtil.xmlToMap(refundData);
//取出加密信息req_info
String req_info = refundXML.get("req_info");
if (req_info != null) {
//解密req_info , 解密步骤如下:
// (1)对加密串A做base64解码,得到加密串B
//(2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->**设置 )
//(3)用key*对加密串B做AES-256-ECB解密
req_info = AESUtil.decryptData(req_info);
Map<String, String> reqXML = WXPayUtil.xmlToMap(req_info);
if (reqXML.get("refund_status") != null && reqXML.get
("refund_status").equals("SUCCESS")) {
jsonObject.put("status", true);
jsonObject.put("out_trade_no", reqXML.get("out_trade_no"));
return jsonObject;
}
}
jsonObject.put("status", false);
return jsonObject;
}
这一步我认为是最难的,因为我曾经用了很久时间才发现问题这里出问题了,然后又花了很长时间解决了。(总共大概5、6个小时,你敢想象。。。)对,就是下面要介绍的类,解密真的是个问题
下面是类AESUtil:
public class AESUtil {
//
private static final String ALGORITHM = "AES/ECB/PKCS7Padding";
static {
Security.addProvider(new BouncyCastleProvider());//提供类库支持
}
private static String var1 = Hash.toHexString(Hash.calcStringHash(AppMessage.getKey()));
private static SecretKeySpec key = new SecretKeySpec(var1.getBytes(), "AES");
/**
* 对加密串做AES-256-ECB解密
*/
public static String decryptData(String base64Data) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM, "BC");//指定解密算法和使用BouncyCastleProvider库
cipher.init(Cipher.DECRYPT_MODE, key);
return new String(cipher.doFinal(Base64.getDecoder().decode(base64Data)),"utf-8");
}
/**
* 对源字符串做AES-256-ECB加密,貌似没用。。我只是拿来测试的
*/
public static String encryptData(String base64Data) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM, "BC");
cipher.init(Cipher.ENCRYPT_MODE, key);
return new String(Base64.getEncoder().encode(cipher.doFinal(base64Data.getBytes("utf-8"))));
}
}
这里参考了两篇博客:
1、 微信退款结果通知报文AES解密
2、 用Java进行AES256-ECB-PKCS7Padding加密
感谢两位作者的分享。
在使用AESUtil时,需要类库支持和jre限制解除,这部分在上面的第二篇博客中给出了介绍。
- BouncyCastle的加/解密类库的下载地址:http://www.bouncycastle.org/latest_releases.html
下载jar包到本地之后,可以选择直接导入到项目或者编译到本地的maven仓库,关于编译jar到本地maven仓库,可以参考我的另一篇博客——安装jar包到本地maven仓库。- Java本身限制**的长度最多128位,而AES256需要的**长度是256位,因此需要到Java官网上下载一个Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files。java1.8版本下载地址
解压缩文件后可以通过阅读readme.txt来设置,不过windows和linux可能稍微不同,官方给出的设置建议适合linux环境,在windows环境下,集成开发环境使用的jre是jdk文件夹里的jre,所以需要多一次配置。比如说你的Java路径是:C:\Program Files\Java,那么你需要同时配置:C:\Program Files\Java\jre1.8.0_144\lib\security和C:\Program Files\Java\jdk1.8.0_144\jre\lib\security,将两个jar替换掉即可。