开发环境
- 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基础,分布式、微服务等主流技术资料,包含大厂面经,学习笔记、源码讲义、项目实战、讲解视频。


希望可以帮助一些想通过自学提升能力的朋友,领取资料,扫码关注一下
记得关注公众号【编码师兄】
领取更多学习资料
相关文章
暂无评论...
