SpringBoot实现小程序微信支付(超级详细)
SpringBoot微信支付开发
·
开发环境
- java1.8
- maven 3.3.9
- springboot 2.1.3.RELEASE
第一步:开通JSAPI支付
第二步:SpringBoot技术对接
先看看微信支付流程
商户系统和微信支付系统主要交互:
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<String, String> unifiedOrder(Map<String, String> reqData) throws Exception { 55 // map格式转xml 方法在下面 56 String reqBody = mapToXml(reqData); 57 // 发起一次统一下单的请求 方法内容在下面 58 String responseBody = requestOnce(WxConfig.payUrl, reqBody); 59 // 将得到的结果由xml格式转为map格式 方法内容在下面 60 Map<String,String> response= processResponseXml(responseBody); 61 // 得到prepayId 62 String prepayId = response.get("prepay_id"); 63 // 组装参数package_str 为什么这样? 因为二次签名微信规定这样的格式 64 String package_str = "prepay_id="+prepayId; 65 Map<String,String> 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<String, String> data, String key, SignType signType) throws Exception { 88 Set<String> 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<String, String> 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<String, String> processResponseXml(String xmlStr) throws Exception { 168 String RETURN_CODE = "return_code"; 169 String return_code; 170 Map<String, String> 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<String, String> 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.<ConnectionSocketFactory>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<String, String> xmlToMap(String strXML) throws Exception { 269 try { 270 Map<String, String> data = new HashMap<String, String>(); 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<String, String> 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<String, String> stringStringMap = prepaidOrder.toWxPayParameters(); 11 // 调起统一支付 12 Map<String, String> 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<String, String> 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基础,分布式、微服务等主流技术资料,包含大厂面经,学习笔记、源码讲义、项目实战、讲解视频。
希望可以帮助一些想通过自学提升能力的朋友,领取资料,扫码关注一下
记得关注公众号【编码师兄】
领取更多学习资料
更多推荐
已为社区贡献2条内容
所有评论(0)