SpringBoot 实现微信扫码支付/Native支付
一、背景
在开发一个捐赠项目时须在pc端接入微信扫码支付(Native 扫码支付),在微信端接入微信公众号支付(Jsapi 支付)。后端使用的是Spring Boot框架,前台采用HTML+css+js 编写。
二、微信扫码支付流程
1、微信支付开发文档:微信支付开发文档
2、微信扫码支付业务流程
3、个人理解的支付流程
①用户在商户平台下单
②商户生成商户订单,并调起微信统一下单接口
③微信生成预支付订单,返回预支付的交易连接(code_url),我们根据code_url 生成二维码,提供给消费者扫码。值得注意的是:微信对于同一商户订单号且同一价格的商品可以生成多个预付单,我们可以设置预付单的有效时间(time_expire),预付单过期后,刷新二维码支付页面便会生成一个预付单,此时会发现二维码变了,即返回的预支付的交易连接(code_url)变了。
④用户扫描二维码,跳出支付页面
⑤用户确认支付,输入密码,支付成功
⑥交易成功后,微信返回支付成功页面,后台会调用回调接口通知给商户交易的支付状态,无论支付成功与否,商户都需要告知微信收到了通知,否则,微信将在半个小时内调起8次回调接口,若都没响应,则会退款给消费者。
三、所需依赖
生成二维码的依赖
在这里插入<!--生成二维码-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.2</version>
</dependency>
解析xml的依赖
<!--用于解析xml,因为微信接收和返回的数据是xml文档-->
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom</artifactId>
<version>1.1</version>
</dependency>
四、获取商品信息并生成二维码
1、引入 jquery.qrcode.min.js 生成二维码
public ModelAndView perOrder(String body) {
// 商品描述
body = body;
// 商户订单号
String out_trade_no = PayCommonUtil.getUniqueOrderId();
// 订单总金额,单位为分
String total_fee = "1";
//获得生成支付二维码的字符串
String result = PayCommonUtil.weiXinPay(total_fee, body, out_trade_no);
//生成二维码result
ModelAndView pay = new ModelAndView("pay");
pay.addObject("qrCodeUrl",result);//前台js生成二维码
return pay;
}
前端HTML
public ModelAndView perOrder(String body) {
// 商品描述
body = body;
// 商户订单号
String out_trade_no = PayCommonUtil.getUniqueOrderId();
// 订单总金额,单位为分
String total_fee = "1";
//获得生成支付二维码的字符串
String result = PayCommonUtil.weiXinPay(total_fee, body, out_trade_no);
//生成二维码result
ModelAndView pay = new ModelAndView("pay");
pay.addObject("qrCodeUrl",result);//前台js生成二维码
return pay;
}
2、ZXing 工具类生成二维码
public ModelAndView perOrder(String body) {
// 商品描述
body = body;
// 商户订单号
String out_trade_no = PayCommonUtil.getUniqueOrderId();
// 订单总金额,单位为分
String total_fee = "1";
//获得生成支付二维码的字符串
String result = PayCommonUtil.weiXinPay(total_fee, body, out_trade_no);
//生成二维码result
boolean b = ZXingUtil.encodeQRCodeImage(result, "UTF-8", "E:\\idea\\wxPayDemo\\src\\main\\resources\\static\\QR\\" + out_trade_no + ".jpg", 300, 300, null);
if (b) {
//将生成的二维码返回给前台
}
}
五、支付配置信息
public interface PayConfigUtil {
String APP_ID="";//微信公众号id
String MCH_ID="";//商户id
String API_KEY="";//API密钥
String UFDOOER_URL="https://api.mch.weixin.qq.com/pay/unifiedorder";//微信统一下单地址
String NOTIFY_URL="";//回调地址
String CREATE_IP="";//发起ip
}
六、工具类
MD5
public class MD5Util {
/**
* @param b
* @return java.lang.String
* @author wxc
* @date 2021/7/16/016 15:13
* @description 将自己转换成可识别的字符串
*/
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];
}
/**
* @param b
* @return java.lang.String
* @author wxc
* @date 2021/7/16/016 15:09
* @description 将字节数组转换成可识别字符串
*/
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();
}
/**
*
* @author wxc
* @date 2021/7/16/016 15:23
* @param origin 被转换的内容
* @param charsetname 字符集
* @return java.lang.String
* @description 获取指定内容的MD5值
*/
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 e) {
}
return resultString;
}
private static final String hexDigits[] ={"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"};
public static String UrlEncode(String src) throws UnsupportedEncodingException{
return URLEncoder.encode(src,"UTF-8").replace("+","%20");
}
}
HttpUtil
/**
* @author wxc
* @date 2021年07月16日 15:34
* @description 用于与微信服务器进行交互
*/
public class WxHttpUtil {
private final static int CONNECT_TIMEOUT=5000;
private final static String DEFAULT_ENCODING="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(CONNECT_TIMEOUT);
conn.setReadTimeout(CONNECT_TIMEOUT);
if(contentType!=null){
conn.setRequestProperty("content-type",contentType);
}
OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING);
if (data==null){
data="";
}
writer.write(data);
writer.flush();
writer.close();
reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING));
StringBuilder sb=new StringBuilder();
String line=null;
while ((line=reader.readLine())!=null){
sb.append(line);
sb.append("\r\n");
}
return sb.toString();
}catch (Exception e){
System.err.println("Error connecting to"+urlStr+":"+e.getMessage());
}finally {
try{
if(reader!=null){
reader.close();
}
}catch (IOException e){
}
}
return null;
}
}
PayCommonUtil
/**
* @author wxc
* @date 2021年07月16日 16:29
* @description
*/
public class PayCommonUtil implements PayConfigUtil{
/**
*
* @author wxc
* @date 2021/7/16/016 16:40
* @param characterEncoding
* @param packageParams
* @return boolean
* @description 验证签名
*/
/**
* 支付成功后,微信返回支付成功的信息
* 为了判断商户收到的信息是否是微信发来的而不是其他渠道发来的
* 微信会把信息封装成一个签名传回来
* 商户收到签名后,进行解析并与本地的比对,数据一样代表成功
*/
public static boolean isTenpaySign(String characterEncoding, SortedMap<Object,Object> packageParams,String key){
StringBuffer sb = new StringBuffer();
Set es=packageParams.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=MD5Util.MD5Encode(sb.toString(),characterEncoding).toLowerCase();
String tenpaySign=((String)packageParams.get("sign")).toLowerCase();
return tenpaySign.equals(mysign);
}
/**
*
* @author wxc
* @date 2021/7/29/029 16:47
* @param characterEncoding
* @param packageParams
* @param key
* @return java.lang.String
* @description 创建sign签名
*/
public static String createSign(String characterEncoding,SortedMap<Object,Object> packageParams,String key){
StringBuffer sb = new StringBuffer();
Set es=packageParams.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 sign=MD5Util.MD5Encode(sb.toString(),characterEncoding).toLowerCase();
return sign;
}
/**
*
* @author wxc
* @date 2021/7/16/016 17:06
* @param parameters 请求数据
* @return java.lang.String
* @description 将请求参数转换为xml格式的字符串
*/
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();
sb.append("<" + k + ">" + v + "</" + k + ">");
}
sb.append("</xml>");
return sb.toString();
}
/**
*
* @author wxc
* @date 2021/7/16/016 20:40
* @param return_code
* @param return_msg
* @return java.lang.String
* @description 通知微信已经收到消息,不要再给我发消息了
*/
public static String setXML(String return_code, String return_msg) {
return "<xml><return_code><![CDATA[" + return_code
+ "]]></return_code><return_msg><![CDATA[" + return_msg
+ "]]></return_msg></xml>";
}
/**
*
* @author wxc
* @date 2021/7/16/016 17:14
* @param length
* @return int
* @description 取出一个指定长度的随机正整数
*/
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));
}
/**
*
* @author wxc
* @date 2021/7/16/016 20:40
* @return java.lang.String
* @description 获取当前时间
*/
public static String getCurrTime(){
Date now=new Date();
SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss");
String s = outFormat.format(now);
return s;
}
/**
*
* @author wxc
* @date 2021/7/16/016 17:27
* @param order_price 价格
* @param body 商品信息
* @param out_trade_no 订单号
* @return java.lang.String
* @description 获取生成二维码的字符串
*/
public static String weiXinPay(String order_price,String body,String out_trade_no){
String appid=PayConfigUtil.APP_ID; //账号信息
String mch_id=PayConfigUtil.MCH_ID; //商家号
String key=PayConfigUtil.API_KEY; //密钥
String currTime=PayCommonUtil.getCurrTime();
String strTime = currTime.substring(8, currTime.length());
String strRandom = PayCommonUtil.buildRandom(4) + "";
String nonce_str=strTime+strRandom;
String expireTime=PayCommonUtil.getOrderExpireTime(2*60*1000L);//二维码有效时间两分钟
/*String order_price="1"; //价格 单位:分
String body="guisdvgu"; //商品名称
String out_trade_no="123456"; //订单号*/
String spbill_url=PayConfigUtil.CREATE_IP;//获取发起电脑ip
String notify_url=PayConfigUtil.NOTIFY_URL;//回调接口
String trade_type="NATIVE";
SortedMap<Object,Object> packageParams=new TreeMap<>();
packageParams.put("appid",appid);
packageParams.put("mch_id",mch_id);
packageParams.put("nonce_str",nonce_str);
packageParams.put("body",body);
packageParams.put("out_trade_no",out_trade_no);
packageParams.put("total_fee",order_price);
packageParams.put("spbill_url",spbill_url);
packageParams.put("notify_url",notify_url);
packageParams.put("trade_type",trade_type);
packageParams.put("time_expire",expireTime);
String sign = PayCommonUtil.createSign("UTF-8",packageParams,key);
packageParams.put("sign",sign);
String requestXML = PayCommonUtil.getRequestXml(packageParams);
System.out.println(requestXML);
String resXml= WxHttpUtil.postData(PayConfigUtil.UFDOOER_URL,requestXML);
System.out.println(resXml);
Map map = XMLUtil.doXMLParse(resXml);
String urlCode=(String) map.get("code_url");
return urlCode;
}
/**
*
* @author wxc
* @date 2021/7/17/017 9:08
* @param request
* @param response
* @description 解析微信返回的结果
*/
public static void weixin_notify(HttpServletRequest request, HttpServletResponse response) throws IOException {
//读取参数
InputStream inputStream;
StringBuffer sb = new StringBuffer();
inputStream=request.getInputStream();
String s;
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
while ((s=in.readLine())!=null){
sb.append(s);
}
in.close();
inputStream.close();
//解析xml成map
Map<String,String> m=new HashMap<String,String>();
m=XMLUtil.doXMLParse(sb.toString());
//过滤空 设置TreeMap
SortedMap<Object,Object> packageParams=new TreeMap<Object,Object>();
Iterator it=m.keySet().iterator();
while (it.hasNext()){
String paramter = (String)it.next();
String parameterValue=m.get(paramter);
String v="";
if(null!=parameterValue){
v=parameterValue.trim();
}
packageParams.put(paramter,v);
}
//账号信息
String key=PayConfigUtil.API_KEY;
String out_trade_no=(String)packageParams.get("out_trade_no");
//判断签名是否正确
if(PayCommonUtil.isTenpaySign("UTF-8",packageParams,key)){
//处理业务开始
String resXml="";
if("SUCCESS".equals((String)packageParams.get("result_code"))){
//支付成功
String mch_id=(String)packageParams.get("mch_id");
System.out.println(mch_id);
System.out.println("支付成功");
resXml="<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
}else {
System.out.println("订单"+out_trade_no+"支付失败");
resXml="<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[报文为空]]></return_msg></xml>";
}
//业务处理完毕
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
}else{
System.out.println("通知签名验证失败");
}
}
/**
*
* @author wxc
* @date 2021/7/17/017 11:31
* @return java.lang.String
* @description 获取唯一订单号
*/
public static String getUniqueOrderId(){
return String.valueOf(SnowflakeIdWorker.getSnowflakeIdWorker().nextId());
}
/**
* 设置微信二维码失效时间,并返回具体失效的时间点
* @param expire 二维码的有效时间,单位是毫秒
* @return
*/
public static String getOrderExpireTime(Long expire){
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
Date now = new Date();
Date afterDate = new Date(now .getTime() + expire);
return sdf.format(afterDate );
}
}
XmlUtil
/**
*
* @author wxc
* @date 2021/7/16/016 18:04
* @description 解析微信xml
*/
public class XMLUtil {
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static Map doXMLParse(String strxml) {
Map m = new HashMap();
try {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if(null == strxml || "".equals(strxml)) {
return null;
}
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();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (JDOMException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return m;
}
/**
* 获取子结点的xml
* @param children
* @return String
*/
@SuppressWarnings("rawtypes")
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();
}
/**
* 微信支付将请求参数转换为xml格式的String
*
* @param paramMap
* @return
*/
@SuppressWarnings("rawtypes")
public static String getRequestXmlQuery(SortedMap<String, String> paramMap) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set set = paramMap.entrySet();
Iterator it = set.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
sb.append("<" + key + ">" + "<![CDATA[" + value + "]]></" + key + ">");
}
sb.append("</xml>");
return sb.toString();
}
/**
* 微信支付将请求参数转换为xml格式的String
*
* @param paramMap
* @return
*/
@SuppressWarnings("rawtypes")
public static String getRequestXml(SortedMap<String, String> paramMap) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set set = paramMap.entrySet();
Iterator it = set.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
if ("attach".equalsIgnoreCase(key) || "body".equalsIgnoreCase(key) || "sign".equalsIgnoreCase(key)) {
sb.append("<" + key + ">" + "<![CDATA[" + value + "]]></" + key + ">");
} else {
sb.append("<" + key + ">" + value + "</" + key + ">");
}
}
sb.append("</xml>");
return sb.toString();
}
}