刚起手学习小程序2周,微信支付也是花了4天,今天第4天做完了代码这块分享下自己的学习成果。
我的框架是springboot mybatis mysql 前端主流用vue 框架不是我搭建的,是分模块的,有1个自己的核心jar 和页面外层的jar
说点能够通用的 这里微信支付需要用到的jar
com.thoughtworks.xstream
xstream
1.4.10
com.squareup.retrofit2
retrofit
2.3.0
com.squareup.retrofit2
converter-simplexml
2.3.0
com.squareup.okhttp3
okhttp
3.2.0
org.bouncycastle
bcprov-jdk15on
1.60
上面的jar有okhttp请求 加密解密 xml转换的
项目目录结构 方便大家更明显的感受
接下来是先发2个公共的文件
* * *
**1.WxPayUtils.java**
package com.alpha.modules.wxpay.utils;
import java.io.Writer;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.naming.NoNameCoder;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
/**
* @Author: Temple
* @Description: Xml-Object 转换
* @Date: 2018年12月17日 11:33:21
*/
public class WxPayUtils {
public static final String ALL_CHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final String LETTER_CHAR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final String NUMBER_CHAR = "0123456789";
public static final Random RANDOM = new Random();
public static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss");
/**
* 生成指定长度的随机字符串(包含数字和字母)
*
* @param length
* 随机字符串长度
* @return
*/
public static String getRandomStr(Integer length) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(ALL_CHAR.charAt(RANDOM.nextInt(ALL_CHAR.length())));
}
return builder.toString();
}
/**
* 生成指定长度的随即纯数字字符串
*
* @param length
* 随即字符串长度
* @return
*/
public static String getRandomNum(Integer length) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(NUMBER_CHAR.charAt(RANDOM.nextInt(NUMBER_CHAR.length())));
}
return builder.toString();
}
/**
* 生成随即纯字母字符串
*
* @param length
* 随即字符串长度
* @return
*/
public static String getRandomLetter(Integer length) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(LETTER_CHAR.charAt(RANDOM.nextInt(LETTER_CHAR.length())));
}
return builder.toString();
}
/**
* 获取随机订单号(系统当前时间+随机数字字符串)
*
* @return
*/
public static String getTradeNo() {
StringBuilder builder = new StringBuilder();
Date date = new Date();
builder.append(SIMPLE_DATE_FORMAT.format(date)).append(getRandomNum(17));
return builder.toString();
}
private static XStream xStream = new XStream(new XppDriver(new NoNameCoder()) {
@Override
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点的转换都增加CDATA标记
// boolean cdata = true;
boolean cdata = false;
@Override
@SuppressWarnings("rawtypes")
public void startNode(String name, Class clazz) {
super.startNode(name, clazz);
}
//// 当对象属性带下划线时,XStream会转换成双下划线,
// 重写这个方法,不再像XppDriver那样调用nameCoder来进行编译,而是直接返回节点名称,避免双下划线出现
@Override
public String encodeNode(String name) {
return name;
}
@Override
protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write("");
} else {
writer.write(text);
}
}
};
}
});
/**
* 对象转xml
*
* @param obj
* @return
*/
public static String objToXML(Object obj) {
// 使用注解设置别名必须在使用之前加上注解类才有作用
xStream.processAnnotations(obj.getClass());
return xStream.toXML(obj);
}
public static Object xmlToObj(String xml, Class objClass) {
Serializer serializer = new Persister();
try {
return serializer.read(objClass, xml);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 签名-MD5
*
* @param signMap
* 待签名Map
* @param key
* 密钥
* @return MD5
*/
public static String signByMD5(Map signMap, String key) {
SortedMap sortedMap = new TreeMap(signMap);
StringBuilder builder = new StringBuilder();
for (Map.Entry entry : sortedMap.entrySet()) {
String mapKey = entry.getKey();
if (!mapKey.equals("sign") && !mapKey.equals("key") && StringUtils.isNotBlank(entry.getValue())) {
builder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
builder.append("key=").append(key);
return DigestUtils.md5Hex(builder.toString()).toUpperCase();
}
/**
* 签名-MD5
*
* @param signObj
* 待签名对象
* @param key
* 密钥
* @return
*/
public static String signByMD5(Object signObj, String key) throws IllegalAccessException {
SortedMap sortedMap = new TreeMap();
StringBuilder builder = new StringBuilder();
Class clazz = signObj.getClass();
Field[] fields = clazz.getDeclaredFields(); // 反射回去对象所有字段,包括私有字段
for (Field field : fields) {
field.setAccessible(true);
if (field.get(signObj) != null) {
String name = field.getName();
XStreamAlias anno = field.getAnnotation(XStreamAlias.class); // 若字段上含有@XStreamAlias注解,则参数名取注解值
Element eleAnno = field.getAnnotation(Element.class); // 若字段上含有@Element注解,则参数名取注解值
if (anno != null) {
name = anno.value();
}
if (eleAnno != null) {
name = eleAnno.name();
}
sortedMap.put(name, field.get(signObj).toString());
}
}
for (Map.Entry entry : sortedMap.entrySet()) {
String mapKey = entry.getKey();
if (!mapKey.equals("sign") && !mapKey.equals("key") && StringUtils.isNotBlank(entry.getValue())) {
builder.append(mapKey).append("=").append(entry.getValue()).append("&");
}
}
builder.append("key=").append(key);
return DigestUtils.md5Hex(builder.toString()).toUpperCase();
}
/**
* 圆转分
* @param amount 圆单位金额
* @return
*/
public static Integer yuanToFen(BigDecimal amount) {
return amount.movePointRight(2).intValue();
}
/**
* 分转圆
* @param amount 分单位金额
* @return
*/
public static Double fenToYuan(BigDecimal amount) {
return amount.movePointLeft(2).doubleValue();
}
/**
* 获取ip地址
* @param request
* @return
*/
public static String getIpAddr() {
InetAddress addr = null;
try {
addr= InetAddress.getLocalHost();
String localip=addr.getHostAddress();
return localip;
} catch (UnknownHostException e) {
return null;
}
}
public static String returnXML(String return_code) {
return " ";
}
}
public static String getXmlString(HttpServletRequest request){
BufferedReader reader = null;
String line = "";
String xmlString = null;
try {
reader = request.getReader();
StringBuffer inputString = new StringBuffer();
while ((line = reader.readLine()) != null) {
inputString.append(line);
}
xmlString = inputString.toString();
request.getReader().close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return xmlString;
}
* * *
2.AESUtils.java AES/ECB/PKCS7Padding加密解密
package com.alpha.modules.wxpay.utils;
import java.security.Security;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class AESUtils {
public static final String ALGORITHM = "AES/ECB/PKCS7Padding";
public static String encode(String data,String key) throws Exception{
Security.addProvider(new BouncyCastleProvider());
String md5Key = DigestUtils.md5Hex(key).toLowerCase();
SecretKeySpec keySpec = new SecretKeySpec(md5Key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encode = Base64.encodeBase64(cipher.doFinal(data.getBytes()));
return new String(encode,"UTF-8");
}
public static String decode(String data,String key) throws Exception{
byte[] decodeBase64 = Base64.decodeBase64(data);
SecretKeySpec keySpec = new SecretKeySpec(DigestUtils.md5Hex(key).toLowerCase().getBytes(), "AES"); //生成加密解密需要的Key
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return new String(cipher.doFinal(decodeBase64), "UTF-8");
}
}
WxConfigure.java
package com.alpha.modules.wxpay.utils;
import com.alpha.common.utils.FileUtils;
public class WxConfigure {
//小程序ID
private static String appID = "wx***";
//小程序secret
private static String secret = "83**";
//商户号
private static String mch_id = "**";
//商户密钥
private static String key = "***";
//支付回调地址
private static String payNotifyUrl="http://***/app/payNotifyUrl";
//退款回调地址
private static String refundNotifyUrl="http://***/app/refundNotifyUrl";
//证书
private static String certPath=FileUtils.getAbsolutePath("certificate/")+"apiclient_cert.p12";
public static String getSecret() {
return secret;
}
public static void setSecret(String secret) {
WxConfigure.secret = secret;
}
public static String getKey() {
return key;
}
public static void setKey(String key) {
WxConfigure.key = key;
}
public static String getAppID() {
return appID;
}
public static void setAppID(String appID) {
WxConfigure.appID = appID;
}
public static String getMch_id() {
return mch_id;
}
public static void setMch_id(String mch_id) {
WxConfigure.mch_id = mch_id;
}
public static String getPayNotifyUrl() {
return payNotifyUrl;
}
public static void setPayNotifyUrl(String payNotifyUrl) {
WxConfigure.payNotifyUrl = payNotifyUrl;
}
public static String getRefundNotifyUrl() {
return refundNotifyUrl;
}
public static void setRefundNotifyUrl(String refundNotifyUrl) {
WxConfigure.refundNotifyUrl = refundNotifyUrl;
}
public static String getCertPath() {
return certPath;
}
public static void setCertPath(String certPath) {
WxConfigure.certPath = certPath;
}
}
上面一写配置自己可以考虑写在yml文件里面,目前我先简单的这样写,域名我是用花生壳的,可以用免费的也可以自己买便宜的2级域名,然后本地调试需要花钱内穿实现通过域名访问本地程序,不懂可以私密我,需要的多的我可以单独写一篇内穿的玩法。
接下来会分享核心代码了,就行把封装的更新下,开始更新
下一篇:小程序之微信支付,从小程序到服务端完整流程和代码