最近项目中要使用微信网页支付功能,现在记录下来,希望能帮到你!
微信网页支付有两种模式
模式二与模式一相比,流程更为简单,不依赖设置的回调支付URL。商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
微信网页支付官方文档链接:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1
一 统一配置类
public class WeChatPayConfig {
/**
* 微信服务号APPID
*/
public static String APPID = "";
/**
* 微信支付的商户号
*/
public static String MCHID = "";
/**
* 微信支付的API密钥
*/
public static String APIKEY = "";
/**
* 微信支付成功之后的回调地址【回调地址必须是公网能够访问的地址】
*/
public static String WECHAT_NOTIFY_URL_PC = "http://xxxxxx/wechatCallBack";
/**
* 微信统一下单API地址
*/
public static String UFDODER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 查询支付结果API地址
*/
public static String ORDERQUERY_URL = "https://api.mch.weixin.qq.com/pay/orderquery";
}
二 公共参数类(封装接口需要的参数,需要其他参数可以自行添加)
@Data
public class WeChatParam implements Serializable {
public String amount;//订单金额【!!!备注:以分为单位!!!!】
public String body;//商品名称
public String orderNo;//商户订单号
public String attach;//附加参数
}
三 工具类
/**
* MD5加密签名
*/
public class MD5Utils {
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString;
}
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}
/**
* 发起post请求工具类
*/
public class HttpUtill {
// 连接超时时间
private static final int CONNECTION_TIMEOUT = 3000;
// 参数编码
private static final String ENCODE_CHARSET = "utf-8";
public static String postData(String urlStr, String data){
return postData(urlStr, data, null);
}
public static String postData(String urlStr, String data, String contentType){
BufferedReader reader = null;
try {
URL url = new URL(urlStr);
URLConnection conn = url.openConnection();
conn.setDoOutput(true);
conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setReadTimeout(CONNECTION_TIMEOUT);
if(contentType != null)
conn.setRequestProperty("content-type", contentType);
OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), ENCODE_CHARSET);
if(data == null)
data = "";
writer.write(data);
writer.flush();
writer.close();
reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), ENCODE_CHARSET));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
sb.append("\r\n");
}
return sb.toString();
} catch (IOException e) {
System.out.println("异常信息xxxxxxxxxxx"+e.getMessage());
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
}
}
return null;
}
/**
* 微信支付工具类
*/
public class WechatPayUtil {
/**
* 校验签名是否正确--规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
* @return boolean
*/
public static boolean isTenpaySign( SortedMap<Object, Object> packageParam, String API_KEY) {
StringBuffer sb = new StringBuffer();
Set es = packageParam.entrySet();
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
String v = (String)entry.getValue();
if(!"sign".equals(k) && null != v && !"".equals(v)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + API_KEY);
//算出摘要
String mysign = MD5Utils.MD5Encode(sb.toString(),"UTF-8").toLowerCase();
String tenpaySign = ((String) packageParam.get("sign")).toLowerCase();
return tenpaySign.equals(mysign);
}
/**
* 生成签名
* @param packageParam 参与生成签名的参数map集合
* @param API_KEY 微信支付的API密钥
* @return
*/
public static String createSign( SortedMap<Object, Object> packageParam, String API_KEY) {
StringBuffer sb = new StringBuffer();
Set es = packageParam.entrySet();
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + API_KEY);
String sign = MD5Utils.MD5Encode(sb.toString(), "UTF-8").toUpperCase();
return sign;
}
/**
* 微信支付请求时,参数格式不是json,是xml格式,详见官方文档
* 将请求参数转换为xml格式的string
* @param parameters 请求参数
* @return
*/
public static String getRequestXml(SortedMap<Object, Object> parameters) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set es = parameters.entrySet();
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {
sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");
} else {
sb.append("<" + k + ">" + v + "</" + k + ">");
}
}
sb.append("</xml>");
return sb.toString();
}
/**
* 取出一个指定长度大小的随机正整数.
*
* @param length 随机数长度
*/
public static int buildRandom(int length) {
int num = 1;
double random = Math.random();
if (random < 0.1) {
random = random + 0.1;
}
for (int i = 0; i < length; i++) {
num = num * 10;
}
return (int) ((random * num));
}
/**
* 获取当前时间 yyyyMMddHHmmss
*/
public static String getCurrTime() {
Date now = new Date();
SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
String s = outFormat.format(now);
return s;
}
/**
* 获取本机IP地址
*/
public static String localIp(){
String ip = null;
Enumeration allNetInterfaces;
try {
allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
List<InterfaceAddress> InterfaceAddress = netInterface.getInterfaceAddresses();
for (InterfaceAddress add : InterfaceAddress) {
InetAddress Ip = add.getAddress();
if (Ip != null && Ip instanceof Inet4Address) {
ip = Ip.getHostAddress();
}
}
}
} catch (SocketException e) {
System.out.println("获取本机Ip失败:异常信息:"+e.getMessage());
}
return ip;
}
}
/**
* 解析微信返回结果工具类(微信返回的结果也是xml格式,此工具类是将xml格式转换成map集合)
*/
public class XMLUtil {
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map doXMLParse(String strxml) throws JDOMException, IOException {
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 = XMLUtil.getChildrenText(children);
}
m.put(k, v);
}
//关闭流
in.close();
return m;
}
/**
* 获取子结点的xml
* @param children
* @return String
*/
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(XMLUtil.getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
}
四 准备工作结束,关键一步,获取支付二维码链接,我这里是将这个方法封装在了WechatPayServer类中了,到时候在serviceImpl类中直接使用就行了。
public class WechatPayServer {
/**
* 获取支付二维码
*
* @param param
* @return
* @throws Exception
*/
public String getCodeUrl(WeChatParam param) throws Exception {
/**
* 账号信息
*/
String appid = WeChatPayConfig.APPID;//微信服务号的appid
String mch_id = WeChatPayConfig.MCHID; //微信支付商户号
String key = WeChatPayConfig.APIKEY; // 微信支付的API密钥
String notify_url = WeChatPayConfig.WECHAT_NOTIFY_URL_PC;//回调地址(注意,这里必须要使用外网的地址)
String ufdoder_url = WeChatPayConfig.UFDODER_URL;//微信下单API地址
String trade_type = "NATIVE"; //类型(网页扫码支付)
/**
* 随机字符串
*/
String currTime = WechatPayUtil.getCurrTime();
String strTime = currTime.substring(8, currTime.length());
String strRandom = WechatPayUtil.buildRandom(4) + "";
String nonce_str = strTime + strRandom;
//修改到期时间
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());//设置起始时间
cal.add(Calendar.MINUTE, 2);//增加一分钟
DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
String expireTime = df.format(cal.getTime());
/**
* 参数封装
*/
SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();
packageParams.put("appid", appid);
packageParams.put("mch_id", mch_id);
packageParams.put("nonce_str", nonce_str);//随机字符串
packageParams.put("body", param.getBody());//支付的商品名称
packageParams.put("out_trade_no", param.getOrderNo());//商户订单号【备注:每次发起请求都需要随机的字符串,否则失败。】
packageParams.put("total_fee", param.getAmount());//支付金额
packageParams.put("spbill_create_ip", WechatPayUtil.localIp());//客户端主机
packageParams.put("notify_url", notify_url);
packageParams.put("trade_type", trade_type);
if (StringUtils.isNotBlank(param.getAttach())) {
packageParams.put("attach", param.getAttach());
}
packageParams.put("time_expire", expireTime);//超时时间
String sign = WechatPayUtil.createSign(packageParams, key); //获取签名
packageParams.put("sign", sign);
String requestXML = WechatPayUtil.getRequestXml(packageParams);//将请求参数转换成String类型
System.out.println("微信支付请求参数的报文" + requestXML);
String resXml = HttpUtill.postData(ufdoder_url, requestXML); //解析请求之后的xml参数并且转换成String类型
Map map = XMLUtil.doXMLParse(resXml);
String urlCode = (String) map.get("code_url");
return urlCode;
}
/**
* 查询支付结果
* @param orderNo 这个参数是支付时的商户订单号
*/
public Map orderquery(String orderNo) {
/**
* 账号信息
*/
String appid = WeChatPayConfig.APPID;//微信服务号的appid
String mch_id = WeChatPayConfig.MCHID; //微信支付商户号
String key = WeChatPayConfig.APIKEY; // 微信支付的API密钥
String queryOrderUrl = WeChatPayConfig.ORDERQUERY_URL; //查询订单API地址
/**
* 随机字符串
*/
String currTime = WechatPayUtil.getCurrTime();
String strTime = currTime.substring(8, currTime.length());
String strRandom = WechatPayUtil.buildRandom(4) + "";
String nonce_str = strTime + strRandom;
/**
* 参数封装
*/
SortedMap<Object, Object> packageParams = new TreeMap<Object, Object>();
packageParams.put("appid", appid);
packageParams.put("mch_id", mch_id);
packageParams.put("nonce_str", nonce_str);//随机字符串
packageParams.put("out_trade_no", orderNo);//商户订单号
/**
* 获取签名
*/
String sign = WechatPayUtil.createSign(packageParams, key);
packageParams.put("sign", sign);
String requestXML = WechatPayUtil.getRequestXml(packageParams);//将请求参数转换成String类型
String resXml = null; //解析请求之后的xml参数并且转换成String类型
try {
resXml = HttpUtill.postData(queryOrderUrl, requestXML); //解析请求之后的xml参数并且转换成String类型
} catch (Exception e) {
e.printStackTrace();
}
try {
return XMLUtil.doXMLParse(resXml);
} catch (JDOMException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
五 最后一步就是微信异步回调接口,这个接口是通知你支付结果的,处理支付后自己的业务逻辑。controller层没啥东西,我就直接写实现类中的方法了,自己写的时候只要注意接口名字跟你上面配置的回调地址名字一样就可以了。
/**
* 微信异步回调
*
* @param request
* @param response
*/
@Override
public void wechatCallBack(HttpServletRequest request, HttpServletResponse response) {
//获取微信异步通知结果
SortedMap<Object, Object> packageParams = null;
try {
packageParams = this.getResultMap(request);
} catch (IOException | JDOMException e) {
e.printStackTrace();
System.out.println("获取异步通知结果出现异常");
}
// 账号信息
String key = WeChatPayConfig.APIKEY; //key
//判断签名是否正确
if (WechatPayUtil.isTenpaySign(packageParams, key)) {
//处理业务开始
String resXml = "";
if ("SUCCESS".equals((String) packageParams.get("result_code"))) {
//支付成功
//String openid = (String)packageParams.get("openid");
String orderNo = (String) packageParams.get("out_trade_no");
try {
this.afterPay(orderNo, 2, null);
} catch (Exception e) {
System.out.println("微信支付成功,但处理后续业务出现异常");
}
System.out.println("支付成功");
//通知微信.异步确认成功.必写.不然微信会一直通知后台.八次之后就认为交易失败了.
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
} else {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[这里你随意发挥]]></return_msg>" + "</xml> ";
}
//处理业务完毕 返回微信异步确认结果
try {
BufferedOutputStream out = new BufferedOutputStream(
response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.out.println("通知签名验证失败");
}
}
六 模式二只有异步通知,对于前后端分离的项目,无法知道支付结果,所以要写一个查询接口,其实跟上面的步骤几乎差不多,只不过请求的接口名字和参数不同罢了,方法同样放在WechatPayServer里面,下面是实现类,只是简单的写了下,具体请求参数和返回参数自己决定吧
/**
* 查询微信支付结果
*
* @param serviceReqDTO
* @return
*/
@Override
public void queryWXOrder() {
Map map = wechatPayServer.orderquery("orderNo");
if (map.get("return_code").equals("SUCCESS") && map.get("result_code").equals("SUCCESS") && map.get("trade_state").equals("SUCCESS")) {
System.out.println("支付成功");
} else {
System.out.println("支付失败");
}
}
有啥错误的地方,请大家及时告诉我,谢谢!