准备条件

开始之前先看一下微信的几个平台:
  1. 微信公众平台:
    是微信公众账号申请入口和管理后台。商户可以在公众平台提交基本资料、业务资料、财务资料申请开通微信支付功能。
    平台入口:http://mp.weixin.qq.com。
  2. 微信开放平台:
    微信开放平台是商户APP接入微信支付开放接口的申请入口,通过此平台可申请微信APP支付。
    平台入口:http://open.weixin.qq.com。
  3. 微信商户平台:
    微信商户平台是微信支付相关的商户功能集合,包括参数配置、支付数据查询与统计、在线退款、代金券或立减优惠运营等功能。
    平台入口:http://pay.weixin.qq.com

刚开始接入的时候有点昏,各种参数需要到不同的平台找, 不像支付宝一样只有一个开发平台,如果是接入微信扫码支付设及到微信公众平台和微信商户平台,如果是手机app微信支付,设及到微信开放平台和微信商户平台

开通支付功能:

有了平台账号之后,然后就是开通支付功能,等待审核通过,当然审核过程有可能被退回,大多是描述信息或者经营类别与营业执照描述不一致

微信支付开发:

下面是微信支付的业务流程时序图, 可以看到与支付宝的支付流程主要流程是差不多的,只不过微信扫码可以在本系统完成支付,没有发生页面跳转,可以自己DIY支付页面,只要将支付链接生成二维码图片即可完成支付

logo
logo

接入微信支付步骤
1. 获取支付SDK

gradle:地址 compile("com.github.wxpay:wxpay-sdk:0.0.3")

2. 配置参数

主要是如下参数, 可以配置在一个属性文件中方面配置

1、APP ID ,应用ID(在公众平台–基本配置模块中)

2、APP Sercret ,应用秘钥(在公众平台–基本配置模块中)32位数字大小写字母

3、API Key,API的秘钥(在商户平台–API安全中设置)

4、mchID , 商户号(在公众平台—微信支付—商户信息)

5、order_api , 统一下单API的接口

6、notify_url 交易成功回调的接口的URL

3. 新建一个参数配置类

WxPcPayConfigImpl.java 继承 WXPayConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* 微信支付PC端 基础属性配置
*/
public class WxPcPayConfigImpl implements WXPayConfig {

private Logger logger = LoggerFactory.getLogger(this.getClass());

// 配置文件地址
private static final String FILE_NAME = "weixin_pc_pay.properties";

// 服务号的应用ID
public static String APP_ID;
// 服务号的应用密钥
public static String APP_SECRET;
// 服务号的配置token
public static String TOKEN;
// 商户号
public static String MCH_ID;
// API密钥
public static String API_KEY;
// 签名加密方式
public static String SIGN_TYPE;
// 微信支付证书
public static String CERT_PATH;
// 异步回调地址
public static String NOTIFY_URL;
// 是否使用沙箱环境
public static boolean IS_USE_SANDBOX;
// 证书
private static byte[] certData;
// INSTANCE
private static WxPcPayConfigImpl INSTANCE;
// 配置对象
private static Configuration configs;
// 文件分隔符
public final static String SF_FILE_SEPARATOR = System.getProperty("file.separator");//文件分隔符
// 二维码图片宽度
public final static int QR_IMG_WIDTH = 300;
// 二维码图片高度
public final static int QR_IMG_HEIGHT = 300;

/**
* 返回配置文件实例
*
* @return
* @throws Exception
*/
public static WxPcPayConfigImpl getInstance() throws Exception {
if (INSTANCE == null) {
synchronized (WxPcPayConfigImpl.class) {
if (INSTANCE == null) {
INSTANCE = new WxPcPayConfigImpl();
}
}
}
return INSTANCE;
}

/**
* 加载微信配置文件
*/
public static synchronized void init() {
if (configs != null) {
return;
}
try {
configs = new PropertiesConfiguration(FILE_NAME);
} catch (ConfigurationException e) {
e.printStackTrace();
}

if (configs == null) {
throw new IllegalStateException("读取配置文件错误" + FILE_NAME);
}
APP_ID = configs.getString("appId");
APP_SECRET = configs.getString("appSecret");
TOKEN = configs.getString("token");
MCH_ID = configs.getString("mchId");
API_KEY = configs.getString("apiKey");
SIGN_TYPE = configs.getString("signType");
CERT_PATH = configs.getString("certPath");
IS_USE_SANDBOX = configs.getBoolean("isUseSandbox");
NOTIFY_URL = configs.getString("notifyUrl");

// 加载证书
File file;
try {
// file = new File(CERT_PATH);
Resource resource = new ClassPathResource(CERT_PATH);
file = resource.getFile();
InputStream certStream = new FileInputStream(file);
certData = new byte[(int) file.length()];
certStream.read(certData);
certStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取 App ID
*
* @return App ID
*/
@Override
public String getAppID() {
return APP_ID;
}
/**
* 获取 Mch ID
*
* @return Mch ID
*/
@Override
public String getMchID() {
return MCH_ID;
}
/**
* 获取 API 密钥
*
* @return API密钥
*/
@Override
public String getKey() {
return API_KEY;
}
/**
* 获取商户证书内容
*
* @return 商户证书内容
*/
@Override
public InputStream getCertStream() {
ByteArrayInputStream certBis;
certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
/**
* HTTP(S) 连接超时时间,单位毫秒
*
* @return
*/
@Override
public int getHttpConnectTimeoutMs() {
return 8000;
}
/**
* HTTP(S) 读数据超时时间,单位毫秒
*
* @return
*/
@Override
public int getHttpReadTimeoutMs() {
return 10000;
}

public byte[] getCertData() {
return certData;
}

public void setCertData(byte[] certData) {
this.certData = certData;
}
}
4. new 一个WXPay对象
1
2
3
WXPay wxAppPay;
WxPcPayConfigImpl pcConfig = WxPcPayConfigImpl.getInstance();
wxpay = new WXPay(pcConfig, WXPayConstants.SignType.MD5, WxPcPayConfigImpl.IS_USE_SANDBOX);
5. 有了”对象”之后就可以发送支付请求了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* PC端微信支付请求
*
* @param domainId 商户订单号取实体类id
* @param amount 充值金额
* @return 处理结果数据
*/
public Map<String, Object> weixinPay(String domainId, BigDecimal amount, HttpServletRequest request) {
Map<String, Object> resultMaps = new HashMap<>();
resultMaps.put("status", "success");

HashMap<String, String> data = new HashMap<>();
String currentUserName = "";
if (AccountUtils.getCurrentUser() != null) {
if (StringUtils.isNotBlank(AccountUtils.getCurrentUser().getFdNickName())) {
currentUserName = AccountUtils.getCurrentUser().getFdNickName();
}
}
String description = currentUserName + " 账户充值";

data.put("body", description); // 商品描述
data.put("out_trade_no", domainId); // 商户订单号
data.put("total_fee", AmountUtils.transAmountToCent(amount)); // 总金额,单位为分
data.put("spbill_create_ip", WxUtils.getRemoteIp(request)); // 发起人IP地址
data.put("notify_url", WxPcPayConfigImpl.NOTIFY_URL); // 异步通知地址
data.put("trade_type", "NATIVE"); // 此处指定为扫码支付
data.put("product_id", domainId); // 商品ID,trade_type=NATIVE时(即扫码支付),此参数必传

Map<String, String> resultMap;
try {
resultMap = wxpay.unifiedOrder(data);
logger.info("微信生成二维码返回xml 转成Json" + JsonFormatUtil.formatJson(resultMap.toString()));
System.out.println(resultMap);
} catch (Exception e) {
e.printStackTrace();
logger.error("微信支付处理异常");
resultMaps.put("status", "failed");
return resultMaps;
}
String returnCode = resultMap.get("return_code");
// 返回结果
if ("SUCCESS".equals(returnCode)) {
String resultCode = resultMap.get("result_code");
// 处理结果
if ("SUCCESS".equals(resultCode)) {
logger.info("订单号:{}生成微信支付码成功", domainId);
String urlCode = resultMap.get("code_url");
// 生成二维码
logger.info("");
// WxUtils.encodeQRCode(urlCode,imgPath);
String imgBase64Str = WxUtils.encodeQRCodeBase64(urlCode);
// 是否是微信支付
resultMaps.put("id", domainId);
resultMaps.put("isWxPay", true);
resultMaps.put("img", imgBase64Str);
} else {
String errCodeDes = resultMap.get("err_code_des");
logger.info("订单号:{}生成微信支付码(系统)失败:{}", domainId, errCodeDes);
resultMaps.put("status", "failed");
return resultMaps;
}
} else {
String returnMsg = resultMap.get("return_msg");
logger.info("(订单号:{}生成微信支付码(通信)失败:{}", domainId, returnMsg);
resultMaps.put("status", "failed");
return resultMaps;
}
return resultMaps;
}

微信支付统一下单接口:

1
2
3
4
5
6
7
8
9
10
/**
* 作用:统一下单<br>
* 场景:公共号支付、扫码支付、APP支付
* @param reqData 向wxpay post的请求数据
* @return API返回数据
* @throws Exception
*/
public Map<String, String> unifiedOrder(Map<String, String> reqData) throws Exception {
return this.unifiedOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
}

6. 生成支付二维码

这里我是将二维码链接字符串转成字符流 然后生成Base64位的图片字符, 只要在 设置src属性值,就可以完成图片展示,不用考虑生成的图片放在那里

7. 支付完成, 处理异步回调
备注: 微信支付是使用的xml进行传输数据,需要将xml转成map,当然微信SDK中也提供了工具类,提供了一些十分用的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
package com.github.wxpay.sdk;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.*;
import java.security.MessageDigest;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import com.github.wxpay.sdk.WXPayConstants.SignType;


public class WXPayUtil {

/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx=0; idx<nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
}
catch (Exception ex) {

}
return data;
}

/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}


/**
* 生成带有 sign 的 XML 格式字符串
*
* @param data Map类型数据
* @param key API密钥
* @return 含有sign字段的XML
*/
public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
return generateSignedXml(data, key, SignType.MD5);
}

/**
* 生成带有 sign 的 XML 格式字符串
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名类型
* @return 含有sign字段的XML
*/
public static String generateSignedXml(final Map<String, String> data, String key, SignType signType) throws Exception {
String sign = generateSignature(data, key, signType);
data.put(WXPayConstants.FIELD_SIGN, sign);
return mapToXml(data);
}


/**
* 判断签名是否正确
*
* @param xmlStr XML格式数据
* @param key API密钥
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
Map<String, String> data = xmlToMap(xmlStr);
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key).equals(sign);
}

/**
* 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
*
* @param data Map类型数据
* @param key API密钥
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
return isSignatureValid(data, key, SignType.MD5);
}

/**
* 判断签名是否正确,必须包含sign字段,否则返回false。
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名方式
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key, signType).equals(sign);
}

/**
* 生成签名
*
* @param data 待签名数据
* @param key API密钥
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key) throws Exception {
return generateSignature(data, key, SignType.MD5);
}

/**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @param signType 签名方式
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals(WXPayConstants.FIELD_SIGN)) {
continue;
}
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("key=").append(key);
if (SignType.MD5.equals(signType)) {
return MD5(sb.toString()).toUpperCase();
}
else if (SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
}
else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}


/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}


/**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
public static String MD5(String data) throws Exception {
java.security.MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}

/**
* 生成 HMACSHA256
* @param data 待处理数据
* @param key 密钥
* @return 加密结果
* @throws Exception
*/
public static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
}

参考于:
http://mp.weixin.qq.com

http://open.weixin.qq.com

http://pay.weixin.qq.com