废话不多说,直接进入主体:
最近在做支付相关的项目,大致需求是用户在公众号发起订单之后调起微信支付。我也是第一次做微信支付,在这里把方法记录下来,也为了给初次做微信支付的同行们提供点经验。
一:接入准备工作
1、要接入入微信支付首先要有载体,也就是是平台,这里我用的是公众号,然后就有一个APPID;
2、然后申请商户,我这里直接注册的微信商户,申请成功之后有个商户ID,也就是mchId;
3、在微信公众号后台绑定商户,一个公众号可以绑定多个商户号;
4、登录商户后台进行配置秘钥、支付授权目录、网页授权域名等。
前期的准备工作在官方网站都有说明。
二:接入API
ps:微信支付有多个产品,我这里介绍JSAPI支付,JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款,如下图:
1、JSAPI下单
这个也就是拿着参数去微信平台请求下单,成功之后会返回一个参数prepay_id(预支付交易会话标识),前端用这个参数才能唤醒微信支付控件;示例代码如下:
Map<String, String> param = new HashMap<>();
//部分参数省略
param.put("openid", openId);
param.put("body", body);
//签名+转xml格式
String xmlStr = WxPayUtils.generateSignedXml(param, key);
//请求
String resultStr = HttpClientUtil.post("https://api.mch.weixin.qq.com/pay/unifiedorder", xmlStr);
//将结果转成Map格式
Map<String, String> map = WxPayUtils.xmlToMap(resultStr);
贴上工具类代码:
public class WxPayUtils {
private static final String PREFIX_XML = "<xml>";
private static final String SUFFIX_XML = "</xml>";
private static final String PREFIX_CDATA = "<![CDATA[";
private static final String SUFFIX_CDATA = "]]>";
private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
private static final String ALGORITHM = "AES";
/**
* 随机生成16位字符串
*
* @return
*/
public static String getRandomStr() {
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 32; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
/**
* MD5签名
*
* @param param 参数
* @param key 微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
* @return
*/
public static String getSignStr(Map<String, String> param, String key) {
StringBuffer sb = new StringBuffer();
param.entrySet().stream().filter(n -> n.getValue() != null).sorted(Map.Entry.comparingByKey()).forEachOrdered(e -> sb.append(e.getKey() + "=" + e.getValue() + "&"));
sb.append("key=" + key);
return MD5Helper.MD5Encode(sb.toString()).toUpperCase();
}
/**
* 解析小程序传过来的wxtoke字符串,包括session_key,openid等
*
* @param token
* @return
*/
public static Map<String, String> getWxTokenMap(String token) {
try {
String tokenStr = AES3.getInstance().decrypt(token);
return JsonHelper.jsonToBean(Map.class, tokenStr);
} catch (Exception e) {
return null;
}
}
/**
* map转xml
*
* @param parm
* @param isAddCDATA
* @return
*/
public static String mapToXml(Map<String, String> parm, boolean isAddCDATA) {
StringBuffer strbuff = new StringBuffer(PREFIX_XML);
if (parm != null) {
for (Map.Entry<String, String> entry : parm.entrySet()) {
strbuff.append("<").append(entry.getKey()).append(">");
if ("attach".equalsIgnoreCase(entry.getKey()) || "body".equalsIgnoreCase(entry.getKey())) {
strbuff.append(PREFIX_CDATA);
if (StrTools.isNotNullOrEmpty(entry.getValue())) {
strbuff.append(entry.getValue());
}
strbuff.append(SUFFIX_CDATA);
} else {
if (StrTools.isNotNullOrEmpty(entry.getValue())) {
strbuff.append(entry.getValue());
}
}
strbuff.append("</").append(entry.getKey()).append(">");
}
}
return strbuff.append(SUFFIX_XML).toString();
}
/**
* xml转map
*
* @param strxml
* @return
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strxml) throws Exception {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if (null == strxml || "".equals(strxml)) {
return null;
}
Map<String, String> m = new HashMap<String, String>();
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if (children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = getChildrenText(children);
}
m.put(k, v);
}
in.close();
return m;
}
public static String getChildrenText(List children) {
StringBuffer sb = new StringBuffer();
if (!children.isEmpty()) {
Iterator it = children.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List list = e.getChildren();
sb.append("<" + name + ">");
if (!list.isEmpty()) {
sb.append(getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
/**
* 生成带有 sign 的 XML 格式字符串
*
* @param data Map类型数据
* @param key API密钥
* @return 含有sign字段的XML
*/
public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
//签名
String signStr = getSignStr(data, key);
data.put("sign", signStr);
//转成xml
return WxPayUtils.mapToXml(data, false);
}
/**
* 接收参数签名校验
*
* @param data
* @param key
* @return
*/
public static boolean isSignatureValid(Map<String, String> data, String key) {
if (!data.containsKey("sign")) {
return false;
}
String oldSign = data.get("sign");
data.remove("sign");
return oldSign.equals(getSignStr(data, key));
}
@SneakyThrows
public static Map<String, String> decryptRefundStrToMap(String reqInfo, String key) {
//初始化
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
SecretKeySpec spec = new SecretKeySpec(MD5Helper.MD5Encode(key).toLowerCase().getBytes(), ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, spec);
byte[] decode = Base64.getDecoder().decode(reqInfo);
return xmlToMap(new String(cipher.doFinal(decode)));
}
/**
* xml解析-支持多层xml
*
* @param xmlStr
* @return
*/
public static Map<String, Object> packXmlToMap(String xmlStr) {
List<Map<String, String>> resultList = iterateWholeXML(xmlStr);
Map<String, Object> retMap = new HashMap<String, Object>();
for (int i = 0; i < resultList.size(); i++) {
Map map = (Map) resultList.get(i);
for (Iterator iterator = map.keySet().iterator(); iterator.hasNext(); ) {
String key = (String) iterator.next();
retMap.put(key, (String) map.get(key));
}
}
return retMap;
}
/**
* 递归解析任意的xml 遍历每个节点和属性
*
* @param xmlStr
*/
private static List<Map<String, String>> iterateWholeXML(String xmlStr) {
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
try {
org.dom4j.Document document = DocumentHelper.parseText(xmlStr);
org.dom4j.Element root = document.getRootElement();
recursiveNode(root, list);
return list;
} catch (DocumentException e) {
e.printStackTrace();
}
return null;
}
/**
* 递归遍历所有的节点获得对应的值
*
* @param
*/
private static void recursiveNode(org.dom4j.Element root, List<Map<String, String>> list) {
// 遍历根结点的所有孩子节点
HashMap<String, String> map = new HashMap<String, String>();
for (Iterator iter = root.elementIterator(); iter.hasNext(); ) {
org.dom4j.Element element = (org.dom4j.Element) iter.next();
if (element == null)
continue;
// 获取属性和它的值
for (Iterator attrs = element.attributeIterator(); attrs.hasNext(); ) {
Attribute attr = (Attribute) attrs.next();
System.out.println(attr);
if (attr == null)
continue;
String attrName = attr.getName();
System.out.println("attrname" + attrName);
String attrValue = attr.getValue();
map.put(attrName, attrValue);
}
// 如果有PCDATA,则直接提出
if (element.isTextOnly()) {
String innerName = element.getName();
String innerValue = element.getText();
map.put(innerName, innerValue);
list.add(map);
} else {
// 递归调用
recursiveNode(element, list);
}
}
}
/**
* 加载独立node
* @param root
* @param map
*/
public static void loadXmlNode(org.dom4j.Element root,Map<String,String> map){
for (Iterator<org.dom4j.Element> elementIterator = root.elementIterator(); elementIterator.hasNext(); ) {
org.dom4j.Element next = elementIterator.next();
if(next.isTextOnly()){
String innerName = next.getName();
String innerValue = next.getText();
map.put(innerName, innerValue);
}else if(!StringUtils.equals("LIST",next.getName())){
loadXmlNode(next,map);
}
}
}
/**
* 加载nodeList
* @param root
* @return
*/
public static List<Map<String, String>> loadXmlNodeList(org.dom4j.Element root){
List<Map<String, String>> list = new ArrayList<>();
Iterator<org.dom4j.Element> listNode = root.elementIterator("TX_INFO").next().elementIterator("LIST");
while (listNode.hasNext()){
Map<String, String> map = new HashMap<>();
Iterator<org.dom4j.Element> elementIterator = listNode.next().elementIterator();
while (elementIterator.hasNext()){
org.dom4j.Element next = elementIterator.next();
map.put(next.getName(), next.getText());
}
list.add(map);
}
return list;
}
}
2、将下单接口返回的prepay_id扔给前端,前端通过微信公众的js调起控件进行支付就行了,这都是前端的事情,和后台没关系。
3、微信支付结果回调
支付结果通知的url在支付请求的时候已经传过了,是字段notify_url,支付结果微信是通过post方法访问我们设置的回调url,接收并解析代码如下:
public String wxNotify(HttpServletRequest request, HttpServletResponse response) {
String resXml = resFailXml;
InputStream inStream;
try {
inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
log.info("wxnotify:微信支付----start----");
// 获取微信调用我们notify_url的返回信息
String result = new String(outSteam.toByteArray(), "utf-8");
System.out.println("接收的微信的回调信息字符串:" + result);
log.info("接收微信支付回调信息str:{}", result);
// 关闭流
outSteam.close();
inStream.close();
// xml转换为map
Map<String, String> resultMap = WxPayUtils.xmlToMap(result);
if ("SUCCESS".equalsIgnoreCase(resultMap.get("return_code"))) {
log.info("微信支付回调返回成功");
String key = "key";//商户秘钥
//校验签名
if (WxPayUtils.isSignatureValid(resultMap, key)) {
log.info("接收微信回调信息,签名通过");
//业务处理
} else {
log.info("接收微信回调信息,签名失败");
}
} else {
log.info("接收微信支付回调信息失败,原因:{}", resultMap.get("return_msg"));
resXml = String.format(resXml, resultMap.get("return_msg"));
}
} catch (Exception e) {
log.error("支付回调发生异常:{}", e.getMessage());
} finally {
try {
// 处理业务完毕
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} catch (IOException e) {
log.error("支付回调发生异常end:{}", e.getMessage());
}
}
System.out.println("最终返回的回调信息" + resXml);
return resXml;
}
注意,在接收到微信的回调之后需要给予应答,让微信方知道我们收到了支付通知,否则微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)。
大概就这么多,微信支付还有其他的一些API可以自己去网站查询,这里只是记录最基础的用法。