微信支付的一些常见问题
2020-02-26 本文已影响0人
呼噜噜睡
微信支付有很多种方式,付款码支付,小程序支付,app支付等等。看了一下官方文档,至少有6种支付方式。不论支付的接口是什么,其接口的调用方式大同小异。下面就来说说最常见的问题。
首先就是生成签名sign,以JSAPI的统一下单为例子:
首先就是发送的请求参数对应的实体类:
@Data
public class WxPayJsApiUnifiedOrder {
private String appid;//微信支付分配的公众账号ID
private String mch_id;//微信支付分配的商户号
private String device_info;//自定义参数,
private String nonce_str;//随机字符串,长度要求在32位以内。
private String sign;//通过签名算法计算得出的签名值
private String sign_type = "MD5";//签名类型
private String body;//商品描述
private String detail;//商品详情
private String attach;//附加数据
private String out_trade_no ;//商户订单号
private String fee_type = "CNY";//标价币种 默认人民币:CNY
private int total_fee;//订单总金额,单位为分
private String spbill_create_ip;//终端IP
private String time_start;//交易起始时间
private String time_expire;//交易结束时间
private String goods_tag;//订单优惠标记
private String notify_url;//异步接收微信支付结果通知的回调地址
private String trade_type;//交易类型
private String product_id;//商品ID
private String limit_pay;//指定支付方式 上传此参数no_credit
private String openid;//用户标识 trade_type=JSAPI时
private String receipt;//电子发票入口开放标识
private String scene_info;//该字段常用于线下活动时的场景信息上报,
}
接下来有一个参数spbill_create_ip,这个是服务器的ip,你可以取本机ip,也可以从request中获取ip,都可以。下面给出这两种获取ip的方法:
/**
* 从请求中获取ip 如果nginx做反向代理,注意配置nginx,否则获取不到最原始的请求ip
* @param request
* @return
* @throws UnknownHostException
*/
public static String getIpFromRequest(HttpServletRequest request) throws UnknownHostException {
String ip = request.getHeader("X-Forwarded-For");
if (ip!=null&&!ip.isEmpty()&& !"unKnown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = ip.indexOf(",");
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
}
ip = request.getHeader("X-Real-IP");
if (ip!=null&&!ip.isEmpty() && !"unKnown".equalsIgnoreCase(ip)) {
return ip;
}
String realIp = request.getRemoteAddr();
if(realIp.contains("localhost")||realIp.contains("127.0.0.1")){
realIp = InetAddress.getLocalHost().getHostAddress();
}
return realIp;
}
/**
* 获取本地ip
* @return
*/
public static String getLocalIp() {
try {
InetAddress candidateAddress = null;
// 遍历所有的网络接口
for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements(); ) {
NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
// 在所有的接口下再遍历IP
for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) {
InetAddress inetAddr = (InetAddress) inetAddrs.nextElement();
if (!inetAddr.isLoopbackAddress()) {// 排除loopback类型地址
if (inetAddr.isSiteLocalAddress()) {
// 如果是site-local地址,就是它了
return inetAddr.getHostAddress();
} else if (candidateAddress == null) {
// site-local类型的地址未被发现,先记录候选地址
candidateAddress = inetAddr;
}
}
}
}
if (candidateAddress != null) {
return candidateAddress.getHostAddress();
}
// 如果没有发现 non-loopback地址.只能用最次选的方案
InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();
return jdkSuppliedAddress.getHostAddress();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
接下来我们需要把实体类对应的实例变为map,也就是从bean-->map。map选取TreeMap,建为String类型有天然的按字典排序的能力:
/**
* 将一个 JavaBean 对象转化为一个 Map
* @param bean 要转化的JavaBean 对象
* @return 转化出来的 Map 对象
* @throws IntrospectionException 如果分析类属性失败
* @throws IllegalAccessException 如果实例化 JavaBean 失败
* @throws InvocationTargetException 如果调用属性的 setter 方法失败
*/
public static Map<String, String> beanToMap(Object bean,Map<String,String> returnMap) {
Class<? extends Object> clazz = bean.getClass();
if(returnMap==null){
returnMap = new HashMap<>();
}
BeanInfo beanInfo = null;
try {
beanInfo = Introspector.getBeanInfo(clazz);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (int i = 0; i < propertyDescriptors.length; i++) {
PropertyDescriptor descriptor = propertyDescriptors[i];
String propertyName = descriptor.getName();
if (!propertyName.equals("class")) {
Method readMethod = descriptor.getReadMethod();
Object value = null;
value = readMethod.invoke(bean, new Object[0]);
if (null != propertyName) {
propertyName = propertyName.toString();
}
if (null != value) {
returnMap.put(propertyName, value.toString());
}
}
}
} catch (IntrospectionException e) {
System.out.println("分析类属性失败");
} catch (IllegalAccessException e) {
System.out.println("实例化 JavaBean 失败");
} catch (IllegalArgumentException e) {
System.out.println("映射错误");
} catch (InvocationTargetException e) {
System.out.println("调用属性的 setter 方法失败");
}
return returnMap;
}
接下来就是生成签名串了:
Map<String,String> paramMap = new TreeMap<>();
beanToMap(wxUnifiedOrder,paramMap);// 参数名称按照字典顺序排序 值为空的参数不参与计算sign值
StringBuilder sb = new StringBuilder();
for (Map.Entry<String,String> entry : paramMap.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
String paramStr = sb.toString();
paramStr = paramStr+"key="+wxConfig.getApiKey();
String sign = Md5Util.md5Digest(paramStr);
paramMap.put("sign",sign);
MD5工具类:
// 对数据进行md5加密,用于生成数字签名
public static String md5Digest(String sourceStr) {
return md5Digest(sourceStr,"UTF-8");
}
public static String md5Digest(String sourceStr, String chartSet) {
if (sourceStr == null||sourceStr.trim().isEmpty()) {
throw new NullPointerException("原字符串不能为NULL。");
}
if (chartSet == null||chartSet.trim().isEmpty()) {
chartSet ="UTF-8";
}
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] result = md5.digest(sourceStr.getBytes(chartSet));
return bytesToHexString(result).toUpperCase();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 转成16进制
public static String bytesToHexString(byte[] bArray) {
StringBuffer sb = new StringBuffer(bArray.length);
String sTemp;
for (int i = 0; i < bArray.length; i++) {
sTemp = Integer.toHexString(0xFF & bArray[i]);
if (sTemp.length() < 2)
sb.append(0);
sb.append(sTemp.toLowerCase());
}
return sb.toString();
}
下面将map转换成xml字符串:
public static String getXmlFromMap(Map<String,String> parame){
StringBuffer buffer = new StringBuffer();
buffer.append("<xml>");
Set set = parame.entrySet();
Iterator iterator = set.iterator();
while(iterator.hasNext()){
Map.Entry entry = (Map.Entry) iterator.next();
String key = (String)entry.getKey();
String value = (String)entry.getValue();
buffer.append("<"+key+">"+value+"</"+key+">");
}
buffer.append("</xml>");
return buffer.toString();
}
接下来将接口地址和xml串传入,就可以调用微信接口了,注意这一步一定要发送编码为UTF-8的请求,否则当你的参数中有中文的时候,微信接口总是返回签名错误:
/**
* POST请求
*
* @param url
* @param xmlStr
* @return
* @throws ParseException
* @throws IOException
*/
public static String doXmlPost(String url, String xmlStr){
CloseableHttpResponse response = null;
String result = null;
try {
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
if(xmlStr!=null&&!xmlStr.isEmpty()){
httpPost.setEntity(new StringEntity(xmlStr, ContentType.create("application/xml", Consts.UTF_8)));
}
response = httpclient.execute(httpPost);
HttpEntity entity = response.getEntity();
result = EntityUtils.toString(response.getEntity(), "UTF-8");
//消耗掉response
EntityUtils.consume(entity);
}catch (Exception e){
e.printStackTrace();
} finally {
try {
response.close();
}catch (Exception e2){
e2.printStackTrace();
}
}
return result;
}
另一个是微信的接口回调:
/**
* 微信JSAPI支付回调
* @return
* 因为是响应给微信的,所以不允许抛出异常了,不论成功或者失败,都要有响应
*/
@RequestMapping("/payNotify")
public void payNotify(HttpServletRequest request, HttpServletResponse response) {
logger.info("微信JSAPI支付回调了");
String returnCode = "FAIL";
String returnMsg = "FAIL";
try {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/xml;charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin", "*");
InputStream in = request.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
out.close();
in.close();
String xmlStr = new String(out.toByteArray(), "utf-8");//xml数据
System.out.println("微信支付回调:"+xmlStr);
XMLSerializer xmlSerializer = new XMLSerializer();
String jsonStr = xmlSerializer.read(xmlStr).toString();
logger.info("微信JSAPI支付回调接口的请求参数:" + jsonStr);
WxPayJsApiNotify wxPayNotify = gson.fromJson(jsonStr, WxPayJsApiNotify.class);
wxPayJsApiNotifyService.payNotify(wxPayNotify);
returnCode = "SUCCESS";
returnMsg = "OK";
}catch (Exception e){
logger.error("微信JSAPI支付回调接口,异常:"+e.getMessage()+e.getCause());
returnCode = "FAIL";
returnMsg = "微信JSAPI支付回调接口,异常:"+e.getMessage()+e.getCause();
e.printStackTrace();
}finally {
try {
String xmlResult = WxUtil.getXmlWithRetCodeAndMsg(returnCode, returnMsg);
logger.info("微信JSAPI支付回调接口,返回:"+xmlResult);
response.getWriter().write(xmlResult);
}catch (Exception e){
e.printStackTrace();
}
}
}
对微信返回进行解析:
XMLSerializer xmlSerializer = new XMLSerializer();
String jsonResult = xmlSerializer.read(xmlResult).toString();
Result wxUnifiedOrderResult = gson.fromJson(jsonResult, Result.class);
当我们想要校验微信的返回或者回调,可能会遇到微信返回的sign跟我们本地计算出来的不一致,一个可能的问题就是xml转为json的时候,有问题,比如:
json-lib包实现xml转json时空值被转为空中括号的问题 <![CDATA[]]> => [],就会导致报错或者sign不一致。我的解决办法是手动删除该值,再加入json:
JSONObject jsonObject= (JSONObject)xmlSerializer.read(xmlResult);
String attach = jsonObject.getString("attach");
if(attach!=null&&attach.equalsIgnoreCase("[]")){
jsonObject.put("attach","");
}
pom.xml:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.10</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<dependency>
<groupId>xom</groupId>
<artifactId>xom</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
如果觉得自己写接口调用太麻烦,可以下载微信官方的接口sdk进行参考或者使用。另一个是github有开源的支付组件,封装了大部分的接口,也可以作为参考。
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>3.0.0</version>
</dependency>
有了这些工具类,对于常见的微信支付接口,足以应付了。对于需要证书的,还需要进行额外处理。