SpringBoot实现小程序微信支付(超级详细)

开发环境

  • java1.8
  • maven 3.3.9
  • springboot 2.1.3.RELEASE

第一步:开通JSAPI支付

第二步:SpringBoot技术对接

先看看微信支付流程

SpringBoot实现小程序微信支付(超级详细)_第1张图片

商户系统和微信支付系统主要交互:

1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API】

2、商户server调用支付统一下单,api参见公共api【统一下单API】

3、商户server调用再次签名,api参见公共api【再次签名】

4、商户server接收支付通知,api参见公共api【支付结果通知API】

5、商户server查询支付结果,api参见公共api【查询订单API】

注意上面有两次签名  

1.配置文件类

 1  2 
 3 public final class WxConfig {
 4     public final static String appId="wxe86f60xxxxxxx"; // 小程序appid
 5     public final static String mchId="15365xxxxx";// 商户ID
 6     public final static String key="Ucsdfl782167bjslNCJD129863skkqoo"; // 跟微信支付约定的密钥
 7     public final static String notifyPath="/admin/wxnotify"; // 回调地址
 8     public final static String payUrl="https://api.mch.weixin.qq.com/pay/unifiedorder"; // 统一下单地址
 9     public final static String tradeType="JSAPI"; // 支付方式
10 
11 }

2.微信工具类,统一下单,签名,生成随机字符串。。

  4 import lombok.extern.slf4j.Slf4j;
  5 import org.apache.http.HttpEntity;
  6 import org.apache.http.HttpResponse;
  7 import org.apache.http.client.HttpClient;
  8 import org.apache.http.client.config.RequestConfig;
  9 import org.apache.http.client.methods.HttpPost;
 10 import org.apache.http.config.RegistryBuilder;
 11 import org.apache.http.conn.socket.ConnectionSocketFactory;
 12 import org.apache.http.conn.socket.PlainConnectionSocketFactory;
 13 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
 14 import org.apache.http.entity.StringEntity;
 15 import org.apache.http.impl.client.HttpClientBuilder;
 16 import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
 17 import org.apache.http.util.EntityUtils;
 18 import org.slf4j.Logger;
 19 import org.slf4j.LoggerFactory;
 20 import org.w3c.dom.Document;
 21 import org.w3c.dom.Element;
 22 import org.w3c.dom.Node;
 23 import org.w3c.dom.NodeList;
 24 
 25 import javax.crypto.Mac;
 26 import javax.crypto.spec.SecretKeySpec;
 27 import javax.xml.XMLConstants;
 28 import javax.xml.parsers.DocumentBuilder;
 29 import javax.xml.parsers.DocumentBuilderFactory;
 30 import javax.xml.parsers.ParserConfigurationException;
 31 import javax.xml.transform.OutputKeys;
 32 import javax.xml.transform.Transformer;
 33 import javax.xml.transform.TransformerFactory;
 34 import javax.xml.transform.dom.DOMSource;
 35 import javax.xml.transform.stream.StreamResult;
 36 import java.io.ByteArrayInputStream;
 37 import java.io.InputStream;
 38 import java.io.StringWriter;
 39 import java.security.MessageDigest;
 40 import java.security.SecureRandom;
 41 import java.time.Instant;
 42 import java.util.*;
 43 
 44 @Slf4j
 45 public class WxUtil {
 46     private static final String WXPAYSDK_VERSION = "WXPaySDK/3.0.9";
 47     private static final String USER_AGENT = WXPAYSDK_VERSION +
 48             " (" + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version") +
 49             ") Java/" + System.getProperty("java.version") + " HttpClient/" + HttpClient.class.getPackage().getImplementationVersion();
 50 
 51     private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
 52     private static final Random RANDOM = new SecureRandom();
 53     // 统一下单接口
 54     public static Map  unifiedOrder(Map reqData) throws Exception {
 55         // map格式转xml 方法在下面
 56         String reqBody = mapToXml(reqData);
 57         // 发起一次统一下单的请求 方法内容在下面
 58         String responseBody = requestOnce(WxConfig.payUrl, reqBody);
 59         // 将得到的结果由xml格式转为map格式 方法内容在下面
 60         Map response= processResponseXml(responseBody);
 61         // 得到prepayId 
 62         String prepayId = response.get("prepay_id");
 63         // 组装参数package_str 为什么这样? 因为二次签名微信规定这样的格式
 64         String package_str = "prepay_id="+prepayId;
 65         Map payParameters = new HashMap<>();
 66         long epochSecond = Instant.now().getEpochSecond();
 67         payParameters.put("appId",WxConfig.appId);
 68         payParameters.put("nonceStr", WxUtil.generateNonceStr());
 69         payParameters.put("package", package_str);
 70         payParameters.put("signType", SignType.MD5.name());
 71         payParameters.put("timeStamp", String.valueOf(epochSecond));
 72         // 二次签名
 73         payParameters.put("paySign", WxUtil.generateSignature(payParameters, WxConfig.key, SignType.MD5));
 74         // 返回签名后的map
 75         return payParameters;
 76     }
 77 
 78 
 79     /**
 80      * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
 81      *
 82      * @param data 待签名数据
 83      * @param key API密钥
 84      * @param signType 签名方式
 85      * @return 签名
 86      */
 87     public static String generateSignature(final Map data, String key, SignType signType) throws Exception {
 88         Set keySet = data.keySet();
 89         String[] keyArray = keySet.toArray(new String[keySet.size()]);
 90         Arrays.sort(keyArray);
 91         StringBuilder sb = new StringBuilder();
 92         for (String k : keyArray) {
 93             if (k.equals("sign")) {
 94                 continue;
 95             }
 96             if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
 97                 sb.append(k).append("=").append(data.get(k).trim()).append("&");
 98         }
 99         sb.append("key=").append(key);
100         if (SignType.MD5.equals(signType)) {
101             return MD5(sb.toString()).toUpperCase();
102         }
103         else if (SignType.HMACSHA256.equals(signType)) {
104             return HMACSHA256(sb.toString(), key);
105         }
106         else {
107             throw new Exception(String.format("Invalid sign_type: %s", signType));
108         }
109     }
110 
111     /**
112      * 生成 MD5
113      *
114      * @param data 待处理数据
115      * @return MD5结果
116      */
117     private static String MD5(String data) throws Exception {
118         MessageDigest md = MessageDigest.getInstance("MD5");
119         byte[] array = md.digest(data.getBytes("UTF-8"));
120         StringBuilder sb = new StringBuilder();
121         for (byte item : array) {
122             sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
123         }
124         return sb.toString().toUpperCase();
125     }
126 
127     public static String generateNonceStr() {
128         char[] nonceChars = new char[32];
129         for (int index = 0; index < nonceChars.length; ++index) {
130             nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
131         }
132         return new String(nonceChars);
133     }
134 
135     public static String mapToXml(Map data) throws Exception {
136        Document document = newDocument();
137         Element root = document.createElement("xml");
138         document.appendChild(root);
139         for (String key: data.keySet()) {
140             String value = data.get(key);
141             if (value == null) {
142                 value = "";
143             }
144             value = value.trim();
145             Element filed = document.createElement(key);
146             filed.appendChild(document.createTextNode(value));
147             root.appendChild(filed);
148         }
149         TransformerFactory tf = TransformerFactory.newInstance();
150         Transformer transformer = tf.newTransformer();
151         DOMSource source = new DOMSource(document);
152         transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
153         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
154         StringWriter writer = new StringWriter();
155         StreamResult result = new StreamResult(writer);
156         transformer.transform(source, result);
157         String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
158         try {
159             writer.close();
160         }
161         catch (Exception ex) {
162         }
163         return output;
164     }
165 
166 // 判断签名是否有效
167     private static Map processResponseXml(String xmlStr) throws Exception {
168         String RETURN_CODE = "return_code";
169         String return_code;
170         Map respData = xmlToMap(xmlStr);
171         if (respData.containsKey(RETURN_CODE)) {
172             return_code = respData.get(RETURN_CODE);
173         }
174 
175         else {
176             throw new Exception(String.format("No `return_code` in XML: %s", xmlStr));
177         }
178 
179         if (return_code.equals("FAIL")) {
180             return respData;
181         }
182         else if (return_code.equals("SUCCESS")) {
183             if (isResponseSignatureValid(respData)) {
184                 return respData;
185             }
186             else {
187                 throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
188             }
189         }
190         else {
191             throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr));
192         }
193     }
194 // 判断签名 
195     private static boolean isResponseSignatureValid(Map data) throws Exception {
196         String signKeyword = "sign";
197         if (!data.containsKey(signKeyword) ) {
198             return false;
199         }
200         String sign = data.get(signKeyword);
201         return generateSignature(data, WxConfig.key, SignType.MD5).equals(sign);
202     }
203 
204 // 发起一次请求
205     private static String requestOnce(String payUrl, String data) throws Exception {
206         BasicHttpClientConnectionManager connManager;
207         connManager = new BasicHttpClientConnectionManager(
208                 RegistryBuilder.create()
209                         .register("http", PlainConnectionSocketFactory.getSocketFactory())
210                         .register("https", SSLConnectionSocketFactory.getSocketFactory())
211                         .build(),
212                 null,
213                 null,
214                 null
215         );
216 
217         HttpClient httpClient = HttpClientBuilder.create()
218                 .setConnectionManager(connManager)
219                 .build();
220         HttpPost httpPost = new HttpPost(payUrl);
221 
222         RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(8000).setConnectTimeout(6000).build();
223         httpPost.setConfig(requestConfig);
224 
225         StringEntity postEntity = new StringEntity(data, "UTF-8");
226         httpPost.addHeader("Content-Type", "text/xml");
227         httpPost.addHeader("User-Agent", USER_AGENT + " " + WxConfig.mchId);
228         httpPost.setEntity(postEntity);
229 
230         HttpResponse httpResponse = httpClient.execute(httpPost);
231         HttpEntity httpEntity = httpResponse.getEntity();
232         return EntityUtils.toString(httpEntity, "UTF-8");
233 
234     }
235 
236 
237 
238     private static String HMACSHA256(String data, String key) throws Exception {
239         Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
240         SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
241         sha256_HMAC.init(secret_key);
242         byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
243         StringBuilder sb = new StringBuilder();
244         for (byte item : array) {
245             sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
246         }
247         return sb.toString().toUpperCase();
248     }
249 
250     private static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
251         DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
252         documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
253         documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
254         documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
255         documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
256         documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
257         documentBuilderFactory.setXIncludeAware(false);
258         documentBuilderFactory.setExpandEntityReferences(false);
259 
260         return documentBuilderFactory.newDocumentBuilder();
261     }
262 
263     private static Document newDocument() throws ParserConfigurationException {
264         return newDocumentBuilder().newDocument();
265     }
266 
267 
268     public static Map xmlToMap(String strXML) throws Exception {
269         try {
270             Map data = new HashMap();
271             DocumentBuilder documentBuilder = newDocumentBuilder();
272             InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
273             org.w3c.dom.Document doc = documentBuilder.parse(stream);
274             doc.getDocumentElement().normalize();
275             NodeList nodeList = doc.getDocumentElement().getChildNodes();
276             for (int idx = 0; idx < nodeList.getLength(); ++idx) {
277                 Node node = nodeList.item(idx);
278                 if (node.getNodeType() == Node.ELEMENT_NODE) {
279                     org.w3c.dom.Element element = (org.w3c.dom.Element) node;
280                     data.put(element.getNodeName(), element.getTextContent());
281                 }
282             }
283             try {
284                 stream.close();
285             } catch (Exception ex) {
286                 // do nothing
287             }
288             return data;
289         } catch (Exception ex) {
290             getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
291             throw ex;
292         }
293 
294     }
295     /**
296      * 日志
297      * @return
298      */
299     private static Logger getLogger() {
300         Logger logger = LoggerFactory.getLogger("wxpay java sdk");
301         return logger;
302     }
303 
304 
305 
306     /**
307      * 判断签名是否正确
308      *
309      * @param xmlStr XML格式数据
310      * @param key API密钥
311      * @return 签名是否正确
312      * @throws Exception
313      */
314     public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
315         Map data = xmlToMap(xmlStr);
316         if (!data.containsKey("sign") ) {
317             return false;
318         }
319         String sign = data.get("sign");
320         return generateSignature(data, key,SignType.MD5).equals(sign);
321     }
322 
323 
324 
325 
326 
327 }            

3.小程序发起请求 组装发起统一下单所需要的参数

 1     @PostMapping("/recharge/wx")
 2     public Map recharge(HttpServletRequest request, @RequestParam(value = "vipType",required = true) VipType vipType) throws Exception {
 3         // 本案例是充值会员 用的时候根据实际情况改成自己的需求
 4         Integer loginDealerId = MySecurityUtil.getLoginDealerId();
 5         // 获取ip地址 发起统一下单必要的参数
 6         String ipAddress = HttpUtil.getIpAddress(request);
 7         // 生成预付订单 存入数据库 回调成功在对订单状态进行修改
 8         PrepaidOrder prepaidOrder = payService.recharge(loginDealerId, vipType, ipAddress);
 9         // 组装统一下单需要的数据map 
10         Map stringStringMap = prepaidOrder.toWxPayParameters();
11         // 调起统一支付
12         Map payParameters =WxUtil.unifiedOrder(stringStringMap);
13         return payParameters;
14     }

生成预付订单代码(根据实际需求生成,此处只是我这的需求,仅供参考)

27 @Service("WXPayService")
28 @Slf4j
29 public class PayServiceImpl implements PayService {
30 
33     @Resource
34     PrepaidOrderDao prepaidOrderDao;
35 
36     @Resource
37     VipDao vipDao;
38 
39     @Resource
40     DealerDao dealerDao;
41 
42     @Resource
43     ApplicationContext applicationContext;
44     @Override
45     @Transactional
46     public PrepaidOrder recharge(Integer dealerId, VipType vipType, String userIp) {
47         Dealer dealer = dealerDao.getDealerById(dealerId);
48         SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
49         String newDate = sdf.format(new Date());
50         Random random = new Random();
51         String orderNumber = newDate + random.nextInt(1000000);
52         BigDecimal amount = null;
53         // 如果不是生产环境  付一分钱
54         if (!applicationContext.getEnvironment().getActiveProfiles()[0].contains("prod")){
55             amount = BigDecimal.valueOf(0.01);
56         }else if (vipType.equals(VipType.YEAR)){
57             amount= BigDecimal.valueOf(999);
58         }else {
59             amount = BigDecimal.valueOf(365);
60         }
61         PrepaidOrder prepaidOrder = new PrepaidOrder();
62         prepaidOrder.setDealerId(dealerId);
63         prepaidOrder.setOpenId(dealer.getOpenId()); // 这个是微信需要的 openid
64         prepaidOrder.setVipType(vipType);
65         prepaidOrder.setUserIp(userIp);  // 这个是微信需要的参数 userIp
66         prepaidOrder.setOrderStatus(OrderStatus.ONGOING);
67         prepaidOrder.setAmount(amount); // 这个是微信需要的参数 total_fee
68         prepaidOrder.setOrderNumber(orderNumber); // 这个是微信需要的参数   out_trade_no
69         // 添加预付订单
70         prepaidOrderDao.addPrepaidOrder(prepaidOrder);
71         return prepaidOrder;// 返回预付订单
72  } 73 }

在实体类做最后的参数封装

 1 @Data
 2 public class PrepaidOrder extends BaseModel {
 3     private String orderNumber;
 4     private Integer dealerId;
 5     private Integer versionNum;
 6     private BigDecimal amount;
 7     private OrderStatus orderStatus=OrderStatus.ONGOING;
 8     private LocalDateTime successTime;
 9     private String userIp;
10     private String openId;
11     private VipType vipType;
12 
13     public Map toWxPayParameters() throws Exception {
14         Map map = new HashMap();
15         map.put("body",getBody());  // 商品名字
16         map.put("appid", WxConfig.appId); // 小程序appid
17         map.put("mch_id", WxConfig.mchId); // 商户id
18         map.put("nonce_str", WxUtil.generateNonceStr()); // 随机字符串
19         map.put("notify_url", AppConst.host+WxConfig.notifyPath); // 回调地址
20         map.put("openid",this.openId); // 发起微信支付的用户的openid
21         map.put("out_trade_no",this.orderNumber); // 订单号
22         map.put("spbill_create_ip",this.userIp); // 发起微信支付的用户的ip地址
23         map.put("total_fee",parseAmount()); // 金额 (单位分)
24         map.put("trade_type",WxConfig.tradeType); // 支付类型
25         // 数据签名   也是第一次签名
26         map.put("sign", WxUtil.generateSignature(map, WxConfig.key, SignType.MD5 ));
27         return map;
28     }
29 
30     public String getBody(){
31         if (vipType.equals(VipType.YEAR)){
32             return "年度会员";
33         }else {
34             return "季度会员";
35         }
36     }
37 
38     public String parseAmount(){
39         BigDecimal multiply = amount.multiply(BigDecimal.valueOf(100));
40         BigDecimal result = multiply;
41         if (multiply.compareTo(BigDecimal.valueOf(1))==0){
42             result = BigDecimal.valueOf(1);
43         }
44         return result.toString();
45     }
46 
47     @Override
48     public String toString() {
49         return "PrepaidOrder{" +
50                 "orderNumber='" + orderNumber + '\'' +
51                 ", dealerId=" + dealerId +
52                 ", versionNum=" + versionNum +
53                 ", amount=" + amount +
54                 ", orderStatus=" + orderStatus +
55                 ", successTime=" + successTime +
56                 ", userIp='" + userIp + '\'' +
57                 ", openId='" + openId + '\'' +
58                 ", vipType=" + vipType +
59                 '}';
60     }
61 }

4.签名类型的枚举类  public enum SignType { MD5, HMACSHA256 } 

5.获取用户IP工具类

 1     public static String getIpAddress(HttpServletRequest request) {
 2         String ip = request.getHeader("x-forwarded-for");
 3         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
 4             ip = request.getHeader("Proxy-Client-IP");
 5         }
 6         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
 7             ip = request.getHeader("WL-Proxy-Client-IP");
 8         }
 9         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
10             ip = request.getHeader("HTTP_CLIENT_IP");
11         }
12         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
13             ip = request.getHeader("HTTP_X_FORWARDED_FOR");
14         }
15         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
16             ip = request.getRemoteAddr();
17         }
18         return ip;
19     }

小程序发起微信支付->controller获取用户必要信息->service生成预付订单->实体类参数封装->WxUtil发起统一下单->返回结果

本人花费2个月时间,整理了一套JAVA开发技术资料,内容涵盖java基础,分布式、微服务等主流技术资料,包含大厂面经,学习笔记、源码讲义、项目实战、讲解视频。

SpringBoot实现小程序微信支付(超级详细)_第2张图片

 

 希望可以帮助一些想通过自学提升能力的朋友,领取资料,扫码关注一下

记得关注公众号【编码师兄

领取更多学习资料

你可能感兴趣的:(java,spring,小程序,微信小程序,微信公众平台)