关于微信应用的支付这个东西,因为官方没有服务器的sample,所以对于我们开发者开发感觉有点困难,不过其实认真的看一下文档感觉也是挺容易的,其实我感觉整个过程中,就是微信支付返回的数据有点不符合人性的阅读而已,使用了XML任何还加上类似以前弄论坛的discuz里面的布局文件的格式,不过怎么说都好,这样设计肯定也有它的原理的。如果对我的文章感兴趣,欢迎订阅我的头条号,一点热,yeehot.com.
我这里就以微信应用支付发起订单生产为例子,关于公众号支付后面再说,不过服务器的订单生成也是使用统一的接口的。
下面我们开始我们支付的学习,我这里直接以Android手机客户端发起调用为例,因为我的IOS客户端没有接入微信支付,只是接入了苹果的内支付,所以就不做介绍了,其实和Android的都一样,都是发送支付的响应然后调用微信客户端,发起调用支付的功能。
更多相关的文档大家可以移步到微信支付文档
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1
关于微信支付的开通这些我就不做介绍了,就是微信认证的步骤。
关于微信的支付步骤就是,生产订单后,然后在客户端根据交易回话标识正确的信息,发起微信支付的调用。
微信订单生产的入口是
URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder
我这里就介绍介必填的参数,因为涉及到太多的参数了,其他参数大家可以看文档。
appid:应用ID ,在微信开发平台可以获取
mch_id:商户号 ,开通微信支付会收到的商家ID
nonce_str:随机字符串,这个随机数我们自己生产
sign:签名,这个我们在代码介绍如何生产
body:商品描述
detail:商品详情
out_trade_no:商户订单号,这个订单号我们需要保持到数据库,用来查询是否成功。
total_fee:交易金额
spbill_create_ip:客户端IP
notify_url:异步返回的接口。这里微信服务器会按照一定的频率向这个地址发送交易订单的信息。所以这个一定是外网的地址。
trade_type:交易类型,这里是APP就填写“APP”
首先我们在服务器端根据用于产品信息然后生产订单。
我们在spring MVC服务器端设计几个参数。分别是我们app的用户名,商品描述,商品详情和金钱。
这几个参数,是我用来记录客户端的用户点击支付的频率,以及等下需要用到的订单生成保存在数据库需要用到的,当然你也可以添加更多的参数,我这里就不做介绍,看各人的需要。
假设我们有一个订单是yeehot.com的会员充值,然后是用Y币作为单位,1个Y币是1元,Y币的作用是可以和站内的其他会员交流,送礼物的花费。我的用户名是一点热。那么我们可以在springMVC服务器这样写接受数据的入口
public final static String WEIXIN_ZHIFU_SECRET = "a15";//替换成你的秘钥
public final static String WEIXIN_APPID = "wx1113";//替换成你的应用ID
public final static String WEIXIN_MID = "12111";//替换你的商家ID
@ResponseBody
@RequestMapping(value = "/wx/payinfo", method = RequestMethod.GET)
public String payOrder(String username, String body, String subject, String money){
try {
if (subject != null)
subject = URLDecoder.decode(subject, "utf-8");
if (body != null)
body = URLDecoder.decode(body, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
接着,我们还需要到微信商家平台,取回我们的商家ID和设置秘钥。取回后,还有在我们的微信开放平台取到我们的应用ID,因为我这里是以移动应用为例子,我们把这些参数放到map里面。
Map<String, String> map = new HashMap<String, String>();
map.put("appid", WEIXIN_APPID);
map.put("mch_id", WEIXIN_MID);
完成了上一步操作后,我们需要产生一个随机数。那么我这里直接就以MD5在加上uuid来生成吧
map.put("nonce_str",MD5(UUID.randomUUID().toString()));
其中MD5生成的方法如下:
/**
* generate MD5
*
* @param src
* @return
* @throws Exception
*/
public static String MD5(String src) {
try {
if (src == null) {
return "";
}
byte[] result = null;
MessageDigest alg = MessageDigest.getInstance("MD5");
result = alg.digest(src.getBytes("utf-8"));
return byte2hex(result);
} catch (Exception e) {
throw new RuntimeException();
}
}
private static String byte2hex(byte[] b) {
if (b == null) {
return "";
}
StringBuffer hs = new StringBuffer();
String stmp = null;
for (int n = 0; n < b.length; n++) {
stmp = Integer.toHexString(b[n] & 0XFF);
if (stmp.length() == 1) {
hs.append("0");
}
hs.append(stmp);
}
return hs.toString();
}
到了这一步,就可以进行订单号的生成了。我们以时间来生成out_trade_no
String tradeNo = getOutTradeNo();
map.put("out_trade_no", tradeNo);
这里的 getOutTradeNo()的方法代码如下:
public static String getOutTradeNo() {
SimpleDateFormat format = new SimpleDateFormat("YYYYMMddHHmmss");
Date date = new Date();
String key = format.format(date);
java.util.Random r = new java.util.Random();
key += r.nextInt();
key = key.substring(0, 15);
return key;
}
完成订单号的生成,我们可以进行到金额、IP,异步回调URL等其他信息的输入
map.put("total_fee", money);
map.put("spbill_create_ip", "192.168.1.1");
map.put("notify_url",
"http://pay.yeehot.com/YeehotProgram/zhifu/wxnotify");
map.put("trade_type", "APP");
到了这一步需要注意的就是签名的方法了。
因为签名是根据我们有多少个参数数据,就需要将多少个参数进行拼接,当然是要除了sign本身
所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA,接着在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。具体生成请看官方文档
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3
我这里就继续用java如何来实现签名的算法:
String signString = creatWXsign(map,WEIXIN_ZHIFU_SECRET);
System.out.println("yeehotsign==" + signString);
map.put("sign", signString);
这里的WEIXIN_ZHIFU_SECRET,是我们刚刚说到的在支付商家平台自定义的一个秘钥,大家可以登录到商家平台自行设置一个秘钥。其中map是我们上面几个步骤已经包含的参数。
public static String creatWXsign(Map<String, String> params, String secret) {
StringBuilder result = new StringBuilder();
List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys);
for (Iterator<String> iterator = keys.iterator(); iterator.hasNext(); ) {
String key = iterator.next();
if (key!=null&&key.length()>0&&!"key".equalsIgnoreCase(key)&&!"sign".equalsIgnoreCase(key)) {
result.append(key).append("=").append(params.get(key));
if (iterator.hasNext()) {
result.append("&");
}
}
}
result.append("&key="+secret);
System.out.println("md5 sign="+result.toString());
return MD5(result.toString());
}
在提交到微信服务器之前,我们还需要将数据转换成XML格式的string类型。这个步骤我就觉得有点让人摸不着头脑。哈哈,实现代码如下:
public String RequestXml2String(Map<String, String> params) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set es = params.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 ("attach".equalsIgnoreCase(k)||"body".equalsIgnoreCase(k)||"sign".equalsIgnoreCase(k)) {
sb.append("<"+k+">"+"<![CDATA["+v+"]]></"+k+">");
}else {
sb.append("<"+k+">"+v+"</"+k+">");
}
}
sb.append("</xml>");
return sb.toString();
}
接着我们可以写一个POST的方法,我这里是直接使用CloseableHttpClient来实现。我这里同时也把MAP键值对转换层XML格式的字符串类型了。然后提交到服务器。
private String PostWX(String url, Map<String ,String> mapinfo){
String postinfo=RequestXml2String(mapinfo);
String result="";
X509TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] xcs,
String string) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] xcs,
String string) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
SSLContext sslcontext;
try {
sslcontext = SSLContext.getInstance("TLS");
sslcontext.init(null, new TrustManager[]{tm}, null);
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (KeyManagementException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ConnectionSocketFactory plainsf = PlainConnectionSocketFactory
.getSocketFactory();
LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory
.getSocketFactory();
Registry<ConnectionSocketFactory> registry = RegistryBuilder
.<ConnectionSocketFactory> create().register("http", plainsf)
.register("https", sslsf).build();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
registry);
// 将最大连接数增加
cm.setMaxTotal(200);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
HttpPost post = new HttpPost(url);
post.setEntity(new StringEntity(postinfo, "UTF-8"));
try {
CloseableHttpResponse response = httpClient.execute(post);
try {
StringBuilder sb2 = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
String line;
while ((line = br.readLine()) != null) {
sb2.append(line);
}
result= sb2.toString();
System.out.println("------------------------------------");
} finally {
response.close();
}
}
catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally {
// 关闭连接,释放资源
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
当提交成功后会返回一串XML格式的数据,如下,我们需要对这些数据进行转换
我们继续
String infoString =PostWX(
"https://api.mch.weixin.qq.com/pay/unifiedorder", map);
Map<String, String> appinfo = new HashMap<String, String>();
Map<String, String> info = new HashMap<String, String>();
JSONObject jsonObject = new JSONObject();
info = doXMLParse(infoString);
这里XML解析的代码如下:
import org.jdom.*;
public static Map<String,String> doXMLParse(String inputinfo) throws JDOMException, IOException {
if(null == inputinfo || "".equals(inputinfo)) {
return null;
}
Map<String,String> m = new HashMap<String,String>();
InputStream in =new ByteArrayInputStream(inputinfo.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 = getChildrenText(children);
}
m.put(k, v);
}
in.close();
return m;
}
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(getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
获取转换的数据后,我们还需要对数据进行签名返回给客户端,这个一定要注意的,不是原来的签名。
try {
info = doXMLParse(infoString);
jsonObject.put("appid", info.get("appid"));
jsonObject.put("prepayid", info.get("prepay_id"));
jsonObject.put("timestamp", String.valueOf(new java.sql.Timestamp(System.currentTimeMillis())));
jsonObject.put("noncestr",
MD5(UUID.randomUUID().toString()));
jsonObject.put("partnerid", info.get("mch_id"));
jsonObject.put("package", "Sign=WXPay");
String signString2 = creatWXsign(appinfo,
WEIXIN_ZHIFU_SECRET);
//这里的签名是新的签名信息,用于客户端调用的。
//这里需要将订单信息保存到数据库,我这里就不写出来,下一节再写
jsonObject.put("sign", signString2);
jsonObject.put("result_code", info.get("result_code"));
} catch (JDOMException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (JSONException e) {
e.printStackTrace();
}
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return jsonObject.toString();
我这里设计返回给客户端的格式是json
{
"result_code": "SUCCESS",
"sign": "121266a0dd51f8981",
"timestamp": "2016-07-29 11:05:58.917",
"partnerid": "121212121",
"noncestr": "bd90122321356a1fdd75f185c4",
"prepayid": "wx20160729195123213213122112",
"package": "Sign=WXPay",
"appid": "wx2412112123233"
}
客户端调用这些参数就可以实现回调了,我以安卓的为例:这些参数我是修改过的,真实的数据就看自己返回的数据。
PayReq req = new PayReq();
req.appId = "wx2412112123233";
req.partnerId = "121212121";
req.prepayId = "wx20160729195123213213122112";
req.nonceStr = "bd90122321356a1fdd75f185c4";
req.timeStamp = "2016-07-29 11:05:58.917";
req.packageValue = "Sign=WXPay";
req.sign = "121266a0dd51f8981";
req.extData = "app data"; // optional
// 在支付之前,如果应用没有注册到微信,应该先调用IWXMsg.registerApp将应用注册到微信
api.sendReq(req);
整个controller的代码如下:
/**
* Created by yeehot on 16/7/29.
*/
@Controller
public class WeiXinPayController {
public final static String WEIXIN_ZHIFU_SECRET = "a15";//替换成你的秘钥
public final static String WEIXIN_APPID = "wx1113";//替换成你的应用ID
public final static String WEIXIN_MID = "12111";//替换你的商家ID
@ResponseBody
@RequestMapping(value = "/wx/payinfo", method = RequestMethod.GET)
public String payOrder(String username, String body, String subject, String money){
{
try {
if (subject != null)
subject = URLDecoder.decode(subject, "utf-8");
if (body != null)
body = URLDecoder.decode(body, "utf-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Map<String, String> map = new HashMap<String, String>();
map.put("appid", WEIXIN_APPID);
map.put("mch_id", WEIXIN_MID);
map.put("nonce_str",MD5(UUID.randomUUID().toString()));
map.put("device_info", "WEB");
map.put("body", body);
map.put("detail", subject);
String tradeNo = getOutTradeNo();
map.put("out_trade_no", tradeNo);
map.put("total_fee", money);
map.put("spbill_create_ip", "192.168.1.1");
map.put("notify_url",
"http://pay.yeehot.com/YeehotProgram/zhifu/wxnotify");
map.put("trade_type", "APP");
String signString = creatWXsign(map,
WEIXIN_ZHIFU_SECRET);
System.out.println("yeehotsign==" + signString);
map.put("sign", signString);
String infoString =PostWX(
"https://api.mch.weixin.qq.com/pay/unifiedorder", map);
//
Map<String, String> appinfo = new HashMap<String, String>();
Map<String, String> info = new HashMap<String, String>();
JSONObject jsonObject = new JSONObject();
try {
info = doXMLParse(infoString);
jsonObject.put("appid", info.get("appid"));
jsonObject.put("prepayid", info.get("prepay_id"));
jsonObject.put("timestamp", String.valueOf(new java.sql.Timestamp(System.currentTimeMillis())));
jsonObject.put("noncestr",
MD5(UUID.randomUUID().toString()));
jsonObject.put("partnerid", info.get("mch_id"));
jsonObject.put("package", "Sign=WXPay");
String signString2 = creatWXsign(appinfo,
WEIXIN_ZHIFU_SECRET);
//这里需要将订单信息保存到数据库,我这里就不写出来,下一节再写
jsonObject.put("sign", signString2);
jsonObject.put("result_code", info.get("result_code"));
} catch (JDOMException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (JSONException e) {
e.printStackTrace();
}
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return jsonObject.toString();
}
private String PostWX(String url, Map<String ,String> mapinfo){
String postinfo=RequestXml2String(mapinfo);
String result="";
X509TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] xcs,
String string) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] xcs,
String string) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
SSLContext sslcontext;
try {
sslcontext = SSLContext.getInstance("TLS");
sslcontext.init(null, new TrustManager[]{tm}, null);
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (KeyManagementException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ConnectionSocketFactory plainsf = PlainConnectionSocketFactory
.getSocketFactory();
LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory
.getSocketFactory();
Registry<ConnectionSocketFactory> registry = RegistryBuilder
.<ConnectionSocketFactory> create().register("http", plainsf)
.register("https", sslsf).build();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
registry);
// 将最大连接数增加
cm.setMaxTotal(200);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
HttpPost post = new HttpPost(url);
post.setEntity(new StringEntity(postinfo, "UTF-8"));
try {
CloseableHttpResponse response = httpClient.execute(post);
try {
StringBuilder sb2 = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
String line;
while ((line = br.readLine()) != null) {
sb2.append(line);
}
result= sb2.toString();
System.out.println("------------------------------------");
} finally {
response.close();
}
}
catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally {
// 关闭连接,释放资源
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* generate MD5
*
* @param src
* @return
* @throws Exception
*/
public static String MD5(String src) {
try {
if (src == null) {
return "";
}
byte[] result = null;
MessageDigest alg = MessageDigest.getInstance("MD5");
result = alg.digest(src.getBytes("utf-8"));
return byte2hex(result);
} catch (Exception e) {
throw new RuntimeException();
}
}
private static String byte2hex(byte[] b) {
if (b == null) {
return "";
}
StringBuffer hs = new StringBuffer();
String stmp = null;
for (int n = 0; n < b.length; n++) {
stmp = Integer.toHexString(b[n] & 0XFF);
if (stmp.length() == 1) {
hs.append("0");
}
hs.append(stmp);
}
return hs.toString();
}
public static String getOutTradeNo() {
SimpleDateFormat format = new SimpleDateFormat("YYYYMMddHHmmss");
Date date = new Date();
String key = format.format(date);
java.util.Random r = new java.util.Random();
key += r.nextInt();
key = key.substring(0, 15);
return key;
}
public static String creatWXsign(Map<String, String> params, String secret) {
StringBuilder result = new StringBuilder();
List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys);
for (Iterator<String> iterator = keys.iterator(); iterator.hasNext(); ) {
String key = iterator.next();
if (key!=null&&key.length()>0&&!"key".equalsIgnoreCase(key)&&!"sign".equalsIgnoreCase(key)) {
result.append(key).append("=").append(params.get(key));
if (iterator.hasNext()) {
result.append("&");
}
}
// System.out.println("md5 sign="+result.toString());
}
result.append("&key="+secret);
System.out.println("md5 sign="+result.toString());
return MD5(result.toString());
}
public String RequestXml2String(Map<String, String> params) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set es = params.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 ("attach".equalsIgnoreCase(k)||"body".equalsIgnoreCase(k)||"sign".equalsIgnoreCase(k)) {
sb.append("<"+k+">"+"<![CDATA["+v+"]]></"+k+">");
}else {
sb.append("<"+k+">"+v+"</"+k+">");
}
}
sb.append("</xml>");
return sb.toString();
}
public static Map<String,String> doXMLParse(String inputinfo) throws JDOMException, IOException {
if(null == inputinfo || "".equals(inputinfo)) {
return null;
}
Map<String,String> m = new HashMap<String,String>();
InputStream in =new ByteArrayInputStream(inputinfo.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 = getChildrenText(children);
}
m.put(k, v);
}
in.close();
return m;
}
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(getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
}
我们还可以到微信平台验证签名的正确性,因为这个关乎是否能够成功调用支付
https://pay.weixin.qq.com/wiki/tools/signverify/
这里就讲到这里为止,如果存在疑问的话,欢迎留言咨询,此外还有一些回调的数据还没有实现,我们留到下一节再讲,欢迎继续关注我的头条号:一点热,yeehot.com,如果有什么问题,欢迎留言咨询,我看到之后会第一时间回复大家的。如果需要转载到其他网站,请与我联系。