接上一篇,领导让我帮忙对接一下微信支付,接到文档之后我一脸懵逼,看了半天之后发现与银行对接大同小异,于是根据微信API要求进行了编码。

先贴上源码:微信支付Demo 首先声明,本篇代码只是自己的积累,项目中的代码封装均为本人编写,本工程是用java8编写

注:必须要在微信小程序控制台申请APPID,KEY,商户号等

所用技术Maven 3.x,IDEA2017,Mysql5.7.x,SpringBoot 2.x,Lombok1.16.x

接下来贴一下maven代码:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter</artifactId> 
 </dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
</dependency> 
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-lang3</artifactId>
	<version>3.4</version>
</dependency> 
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency> 
<dependency>
	<groupId>dom4j</groupId>
	<artifactId>dom4j</artifactId>
</dependency>
<dependency>
	<groupId>xmlpull</groupId>
	<artifactId>xmlpull</artifactId>
	<version>1.1.3.1</version>
</dependency>
<dependency>
	<groupId>net.sf.kxml</groupId>
	<artifactId>kxml2</artifactId>
	<version>2.3.0</version>
</dependency>
<dependency>
	<groupId>com.thoughtworks.xstream</groupId>
	<artifactId>xstream</artifactId>
	<version>1.4.2</version>
</dependency> 
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<version>1.16.22</version>
</dependency>
<dependency>
	<groupId>commons-httpclient</groupId>
	<artifactId>commons-httpclient</artifactId>
	<version>3.1</version>
</dependency>
<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
	<version>4.5.2</version>
</dependency> 

maven依赖加入完成之后,就开始进行项目的封装了

微信小程序支付API

先从统一下单开始:

在这里需要注意,生成的随机算法以及签名戳

这里我直接贴上代码:

import java.util.Random;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * @author ChoviWu
 * @date 2019/1/3
 * Description :
 */
public class OrderUtils {

	private static final AtomicInteger NUMBER = new AtomicInteger(1);

	private static final AtomicBoolean BOOLEAN = new AtomicBoolean(false);

	private static Random rnd = new Random();
	/**
	 * 随机串 生成签名
	 */
	public static final  String RANDOM_NONECE  = "abcdefghijklmnopqrstuvwxyz0123456789";
	/**
	 * 针对微信支付生成商户订单号,为了避免微信商户订单号重复(下单单位支付),
	 *
	 * @return
	 */
	public static String generateOrderSN() {
		StringBuffer orderSNBuffer = new StringBuffer();
		orderSNBuffer.append(System.currentTimeMillis());
		orderSNBuffer.append(getRandomStringByLength(7));
		return orderSNBuffer.toString();
	} 
	/**
	 * 生成随机数
	 * @param length
	 * @return
	 */
	public static String getRandomStringByLength(int length) {
		Random random = new Random();
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < length; i++) {
			int number = random.nextInt(RANDOM_NONECE.length());
			sb.append(RANDOM_NONECE.charAt(number));
		}
		return sb.toString();
	} 
}

’这个类为商户生成的32位随机串,这里就不介绍了

接下来是需要生成唯一的签名Sign(这里的算法我们用MD5加密),如果不严格进行加签,请求到微信会报签名错误

下面我来贴一下工具类: 因为要进行拼接,我利用反射对传入的字段进行取值并拼接,然后进行签名加密

import com.example.demo.core.annotation.Sign;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.util.*;

public class ReflectUtils {

	private static final Logger LOG = LoggerFactory.getLogger(ReflectUtils.class);
	
	public static <T> Map<String,Object> reflectBean(T bean){
		List<Field> list = ClassUtils.getClassFields(bean);
		if(CollectionUtils.isEmpty(list)){
			return null;
		}
		Map map = new HashMap(16);
		for (Field field : list){
			field.setAccessible(true);
			try {
				Object value = field.get(bean);
				map.put(field.getName(),value);
			} catch (IllegalAccessException e) {
				e.printStackTrace();
				LOG.error("error map : {}",e);
				continue;
			}
		}
		LOG.info("获取Bean : {}",map);
		return map;
	}
	public static <T> T generatorBean(T bean) {
		List<Field> list = ClassUtils.getClassFields(bean);
		if (CollectionUtils.isEmpty(list)) {
			return null;
		}
		//ASCII字母排序C
		list.sort((o1, o2) -> o1.getName().compareTo(o2.getName()));
		//必有一个签名字段
		String type = Constants.MD5;
		//签名字段
		Field signField = null;
		//拼接的StringBuilder
		StringBuilder sb = new StringBuilder("");
		for (int i = 0; i < list.size(); i++) {
			Field field = list.get(i);
			field.setAccessible(true);
			try {
				Object value = field.get(bean);
				if (field.isAnnotationPresent(Sign.class)) {
     				//这里可能还需要定义一个忽略的字段,用来标记,但不传输
					Sign sign = field.getAnnotation(Sign.class);
					type = sign.type();
					signField = field;
				}
				// 签名类型
				if(Constants.SIGN_TYPE.equals(field.getName())){
					field.set(bean,type);
				}

				if (StringUtils.isEmpty(sb)) {
					sb.append(field.getName()).append("=").append(value);
				} else {
					//如果是拼接到最后一个元素,进行签名
					if (i == list.size() - 1) {
						try {
							if (!StringUtils.isEmpty(value)) {
								sb.append("&").append(field.getName()).append("=").append(value);
							}
							signField.set(bean, toSign(sb, type));
							break;
						} catch (IllegalAccessException e) {
							e.printStackTrace();
						}
					}
					//过滤为null的value,不参与拼接
					if (StringUtils.isEmpty(value)) {
						continue;
					}
					sb.append("&").append(field.getName()).append("=").append(value);
				}
			} catch (IllegalAccessException e) {
				e.printStackTrace();
				LOG.error("error map : {}", e);
				continue;
			}

		}
		return bean;
}


	/**
	 * 生成签名
	 * MD5
	 *
	 * @param str
	 * @return
	 */
	public static String toSign(StringBuilder str, String signType) {
		StringBuilder sb = str.append("&key=").append(WxUtils.KEY);
		String sign = null;
		sign = new String(sb.toString());
		LOG.info("生成对象成功: {}", sign);
		if (Constants.MD5.equals(signType)) {
			return MD5Utils.MD5Encode(sign).toUpperCase();
		}
		return ShA256Utils.sha256_HMAC(sign, WxUtils.KEY).toUpperCase();
	}
}
`

上面的加签过程依赖了两个工具类,这里贴过来:

import com.yixuan.domain.wxpay.BaseRequest;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class ClassUtils {

	/**
	 * 继承 反射获取字段
	 * @param bean
	 * @param <T>
	 * @return
	 */
	public static <T> List<Field> getClassFields(T bean){
		return sortField(bean.getClass(),new ArrayList<>());
	} 
	/**
	 * 递归获取字段
	 * @param tClass 轮询到的当前类
	 * @param list   字段list
	 * @return  返回字段的List
	 */
	private static <T> List<Field> sortField(Class<T> tClass,List<Field> list){
		if(StringUtils.isEmpty(tClass)){
			return list;
		}
		Field[] fields = tClass.getDeclaredFields();
		for (Field field : fields){
			list.add(field);
		}
		Class clazz = tClass.getSuperclass();
		if(clazz==null){
			return list;
		}
		return sortField(clazz.getSuperclass(),list);
	}
}

以及MD5加密的(网上有)

在这里注意,我在调试过程中总是出现签名错误,经调试,Md5加密与微信支付的加密后的不对,这里可以在微信官方进行调试微信公众平台支付接口调试工具

**注:在SHA256加密算法中,如果你的参数里有了中文等字符时,需要在加密前转为UTF-8,否则会 报 “签名错误” **

<xml>
	<return_code><![CDATA[FAIL]]></return_code>
	<return_msg><![CDATA[签名错误]]></return_msg>
</xml>

在这我先贴上SHA256工具类:

package com.yixuan.utils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class ShA256Utils {

    /**
     * 将加密后的字节数组转换成字符串
     *
     * @param b 字节数组
     * @return 字符串
     */
    private static String byteArrayToHexString(byte[] b) {
        StringBuilder hs = new StringBuilder();
        String stmp;
        for (int n = 0; b!=null && n < b.length; n++) {
            stmp = Integer.toHexString(b[n] & 0XFF);
            if (stmp.length() == 1)
                hs.append('0');
            hs.append(stmp);
        }
        return hs.toString().toLowerCase();
    }
    /**
     * sha256_HMAC加密
     * @param message 消息
     * @param secret  秘钥
     * @return 加密后字符串
     */
    public static String sha256_HMAC(String message, String secret) {
        String hash = "";
        try {
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
            sha256_HMAC.init(secret_key);
			//重点  : *这里需要进行UTF-8转码,为了防止中文字符匹配签名失败**
            byte[] bytes = sha256_HMAC.doFinal(message.getBytes("UTF-8"));
            hash = byteArrayToHexString(bytes);
            System.out.println(hash);
        } catch (Exception e) {
            System.out.println("Error HmacSHA256 ===========" + e.getMessage());
        }
        System.out.println("Sha256 生成结果:" + hash);
        return hash;
    }
}

import java.security.MessageDigest;

/**
 * @author ChoviWu
 * @date 2019/1/4
 * Description :
 */
public class MD5Utils {
	private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7",
			"8", "9", "a", "b", "c", "d", "e", "f"};

	/**
	 * 转换字节数组为16进制字串
	 * @param b 字节数组
	 * @return 16进制字串
	 */
	public static String byteArrayToHexString(byte[] b) {
		StringBuilder resultSb = new StringBuilder();
		for (byte aB : b) {
			resultSb.append(byteToHexString(aB));
		}
		return resultSb.toString();
	}

	/**
	 * 转换byte到16进制
	 * @param b 要转换的byte
	 * @return 16进制格式
	 */
	private static String byteToHexString(byte b) {
		int n = b;
		if (n < 0) {
			n = 256 + n;
		}
		int d1 = n / 16;
		int d2 = n % 16;
		return hexDigits[d1] + hexDigits[d2];
	}

	/**
	 * MD5编码
	 * @param origin 原始字符串
	 * @return 经过MD5加密之后的结果
	 */
	public static String MD5Encode(String origin) {
		String resultString = null;
		try {
			resultString = origin;
			MessageDigest md = MessageDigest.getInstance("MD5");
			md.update(resultString.getBytes("UTF-8"));
			resultString = byteArrayToHexString(md.digest());
		} catch (Exception e) {
			e.printStackTrace();
		}
		return resultString;
	}
}

到这一步,我们传入的对象基本已经可以拼接好并生成签名串了,由于微信所需要的是发送xml格式的,所以,我们需要对bean进行格式转化: 这里我就不贴代码了

在bean类加入注解是不够的,还需要在请求微信接口之前进行xml格式转化,这里我封装了一个工具类,如下:

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.StringReader;

public class XmlUtils {

	private static final Logger LOG = LoggerFactory.getLogger(XmlUtils.class);

	public static String beanToXml(Object obj){
		XStream xstream = new XStream(new DomDriver("UTF-8"));
		// 识别obj类中的注解
		xstream.processAnnotations(obj.getClass());
		// 以格式化的方式输出XML
		String xml = xstream.toXML(obj);
		//在这里,xstream会把bean的单下划线转化为双下划綫,这里要进行替换字符串
		xml = xml.replace("__","_");
		LOG.info("请求参数 : \n" + xml);
		return xml;
	}
	//微信返回我们的数据,转化为bean类
	public static <T> T toBean(String xmlStr, Class<T> cls) {
		T t = null;
		try {
			JAXBContext context = JAXBContext.newInstance(cls);
			Unmarshaller unmarshaller = context.createUnmarshaller();
			t = (T)unmarshaller.unmarshal(new StringReader(xmlStr));
		} catch (JAXBException e) {
			e.printStackTrace();
		}
		return t;
	}

}

至此,一切都准备就绪了,我们可以请求微信的下单接口了:

 /**
     * 下单
     * @param unifiedOrder  自己封装的下单的bean类,与微信方保持一致
     * @return
     */
    public static UnifiedOrderReturn unifiedOrder(UnifiedOrder  unifiedOrder){
        System.out.println(JsonUtil.toJson(unifiedOrder));
		//http请求微信接口,这里的参数是上面我们封装的反射调用和转化xml的类
        String response = HttpUtil.sendHttpPostXml(UNIFIEDORDER,XmlUtils.beanToXml(ReflectUtils.appendToString(unifiedOrder)));
		//微信返回xml  我们对其进行转化
        UnifiedOrderReturn unifiedOrderReturn =XmlUtils.toBean(response,UnifiedOrderReturn.class);
		//处理逻辑
        if(Constants.FAIL.equals(unifiedOrderReturn.getReturn_code())){
            //TODO 失败
            System.out.println(JsonUtils.toJson(unifiedOrderReturn));
        }
        return unifiedOrderReturn;
    }

逻辑业务部分,我就不写了,一般都是现在本地插入记录,然后请求微信方,下单成功之后,下一步前端会调键盘唤醒支付,用户支付完成则微信端会异步到商户的系统(可见上篇的流程图)成功接口,商户进行修改订单状态。

至此,本篇文章结束,该部分只以下单为例,其余的也适用。

源码:微信支付Demo 欢迎star