概述
详细
一、准备工作
先开通微信公众号,再开通微信公众号里面的微信支付功能,这些是前提条件,多说一句,申请开通微信公众号需要等待审核,然后在开通微信支付功能,还得等待审核,前前后后耗时得好几天。
关于准备工作,再看看微信官方关于“微信支付”的介绍,官方地址 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1 。这个是文档的准备,大概可以理解到微信支付有哪些模式,然后大概是怎样一个东东。
然后重点看看如下几个,实际上需要准备的东西,红色花掉的部分(PayConfigUtil类里面),需要根据自己的实际情况填写:
其中APP_ID和APP_SECRET可以在公众平台找着,MCH_ID和API_KEY则在商户平台找到,特别是API_KEY要在商户平台设置好,对于“微信扫码支付模式二”(支付与回调)实际只会用到APP_ID、MCH_ID和API_KEY,其他的都不用。
二、程序实现
这里使用spring mvc做一个购买商品,微信扫码支付的演示。先项目代码截图,
以下摘取重点环节的代码说明下:
1、首先是接入微信接口,获取微信支付二维码。
package
com.demodashi;
import
java.util.Map;
import
java.util.SortedMap;
import
java.util.TreeMap;
import
javax.inject.Named;
import
com.demodashi.pay.util.HttpUtil;
import
com.demodashi.pay.util.PayToolUtil;
import
com.demodashi.pay.util.PayConfigUtil;
import
com.demodashi.pay.util.XMLUtil4jdom;
@Named
(
"userService"
)
public
class
UserServiceImpl
implements
UserService {
@Override
public
String weixinPay(String userId, String productId)
throws
Exception {
String out_trade_no =
""
+ System.currentTimeMillis();
//订单号 (调整为自己的生产逻辑)
// 账号信息
String appid = PayConfigUtil.APP_ID;
// appid
//String appsecret = PayConfigUtil.APP_SECRET; // appsecret
String mch_id = PayConfigUtil.MCH_ID;
// 商业号
String key = PayConfigUtil.API_KEY;
// key
String currTime = PayToolUtil.getCurrTime();
String strTime = currTime.substring(
8
, currTime.length());
String strRandom = PayToolUtil.buildRandom(
4
) +
""
;
String nonce_str = strTime + strRandom;
// 获取发起电脑 ip
String spbill_create_ip = PayConfigUtil.CREATE_IP;
// 回调接口
String notify_url = PayConfigUtil.NOTIFY_URL;
String trade_type =
"NATIVE"
;
SortedMap
new
TreeMap
packageParams.put(
"appid"
, appid);
packageParams.put(
"mch_id"
, mch_id);
packageParams.put(
"nonce_str"
, nonce_str);
packageParams.put(
"body"
,
"可乐"
);
//(调整为自己的名称)
packageParams.put(
"out_trade_no"
, out_trade_no);
packageParams.put(
"total_fee"
,
"10"
);
//价格的单位为分
packageParams.put(
"spbill_create_ip"
, spbill_create_ip);
packageParams.put(
"notify_url"
, notify_url);
packageParams.put(
"trade_type"
, trade_type);
String sign = PayToolUtil.createSign(
"UTF-8"
, packageParams,key);
packageParams.put(
"sign"
, sign);
String requestXML = PayToolUtil.getRequestXml(packageParams);
System.out.println(requestXML);
String resXml = HttpUtil.postData(PayConfigUtil.UFDODER_URL, requestXML);
Map map = XMLUtil4jdom.doXMLParse(resXml);
String urlCode = (String) map.get(
"code_url"
);
return
urlCode;
}
}
|
以上代码会按照微信支付的协议,生成类似这样格式的URL:weixin://wxpay/bizpayurl?pr=pIxXXXX
2、根据以上方法所产生的URL生成二维码,这里采用我采用的是google的core.jar包来生成二维码
@ResponseBody
@RequestMapping
(
"/qrcode.do"
)
public
void
qrcode(HttpServletRequest request, HttpServletResponse response,
ModelMap modelMap) {
try
{
String productId = request.getParameter(
"productId"
);
String userId =
"user01"
;
String text = userApplication.weixinPay(userId, productId);
//根据url来生成生成二维码
int
width =
300
;
int
height =
300
;
//二维码的图片格式
String format =
"gif"
;
Hashtable hints =
new
Hashtable();
//内容所使用编码
hints.put(EncodeHintType.CHARACTER_SET,
"utf-8"
);
BitMatrix bitMatrix;
try
{
bitMatrix =
new
MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
QRUtil.writeToStream(bitMatrix, format, response.getOutputStream());
}
catch
(WriterException e) {
e.printStackTrace();
}
}
catch
(Exception e) {
}
}
|
上面代码中涉及到几个工具类:PayConfigUtil、PayCommonUtil、HttpUtil和XMLUtil,其中PayConfigUtil放的就是上面提到一些配置及路径,PayCommonUtil涉及到了获取当前事件、产生随机字符串、获取参数签名和拼接xml几个方法,代码如下:
package
com.demodashi.pay.util;
import
java.text.SimpleDateFormat;
import
java.util.Date;
import
java.util.Iterator;
import
java.util.Map;
import
java.util.Set;
import
java.util.SortedMap;
public
class
PayToolUtil {
/**
* 是否签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
* @return boolean
*/
public
static
boolean
isTenpaySign(String characterEncoding, SortedMap
StringBuffer sb =
new
StringBuffer();
Set es = packageParams.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
(!
"sign"
.equals(k) &&
null
!= v && !
""
.equals(v)) {
sb.append(k +
"="
+ v +
"&"
);
}
}
sb.append(
"key="
+ API_KEY);
//算出摘要
String mysign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toLowerCase();
String tenpaySign = ((String)packageParams.get(
"sign"
)).toLowerCase();
//System.out.println(tenpaySign + " " + mysign);
return
tenpaySign.equals(mysign);
}
/**
* @author
* @date 2016-4-22
* @Description:sign签名
* @param characterEncoding
* 编码格式
* @param parameters
* 请求参数
* @return
*/
public
static
String createSign(String characterEncoding, SortedMap
StringBuffer sb =
new
StringBuffer();
Set es = packageParams.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
(
null
!= v && !
""
.equals(v) && !
"sign"
.equals(k) && !
"key"
.equals(k)) {
sb.append(k +
"="
+ v +
"&"
);
}
}
sb.append(
"key="
+ API_KEY);
String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return
sign;
}
/**
* @author
* @date 2016-4-22
* @Description:将请求参数转换为xml格式的string
* @param parameters
* 请求参数
* @return
*/
public
static
String getRequestXml(SortedMap
StringBuffer sb =
new
StringBuffer();
sb.append(
"
);
Set es = parameters.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 +
">"
+
"
}
else
{
sb.append(
"<"
+ k +
">"
+ v +
""
+ k +
">"
);
}
}
sb.append(
""
);
return
sb.toString();
}
/**
* 取出一个指定长度大小的随机正整数.
*
* @param length
* int 设定所取出随机数的长度。length小于11
* @return int 返回生成的随机数。
*/
public
static
int
buildRandom(
int
length) {
int
num =
1
;
double
random = Math.random();
if
(random <
0.1
) {
random = random +
0.1
;
}
for
(
int
i =
0
; i < length; i++) {
num = num *
10
;
}
return
(
int
) ((random * num));
}
/**
* 获取当前时间 yyyyMMddHHmmss
*
* @return String
*/
public
static
String getCurrTime() {
Date now =
new
Date();
SimpleDateFormat outFormat =
new
SimpleDateFormat(
"yyyyMMddHHmmss"
);
String s = outFormat.format(now);
return
s;
}
}
|
HttpUtil类如下:
package
com.demodashi.pay.util;
import
java.io.BufferedReader;
import
java.io.IOException;
import
java.io.InputStreamReader;
import
java.io.OutputStreamWriter;
import
java.net.URL;
import
java.net.URLConnection;
/**
* http工具类,负责发起post请求并获取的返回
*/
public
class
HttpUtil {
private
final
static
int
CONNECT_TIMEOUT =
5000
;
// in milliseconds
private
final
static
String DEFAULT_ENCODING =
"UTF-8"
;
public
static
String postData(String urlStr, String data){
return
postData(urlStr, data,
null
);
}
public
static
String postData(String urlStr, String data, String contentType){
BufferedReader reader =
null
;
try
{
URL url =
new
URL(urlStr);
URLConnection conn = url.openConnection();
conn.setDoOutput(
true
);
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(CONNECT_TIMEOUT);
if
(contentType !=
null
)
conn.setRequestProperty(
"content-type"
, contentType);
OutputStreamWriter writer =
new
OutputStreamWriter(conn.getOutputStream(), DEFAULT_ENCODING);
if
(data ==
null
)
data =
""
;
writer.write(data);
writer.flush();
writer.close();
reader =
new
BufferedReader(
new
InputStreamReader(conn.getInputStream(), DEFAULT_ENCODING));
StringBuilder sb =
new
StringBuilder();
String line =
null
;
while
((line = reader.readLine()) !=
null
) {
sb.append(line);
sb.append(
"\r\n"
);
}
return
sb.toString();
}
catch
(IOException e) {
//logger.error("Error connecting to " + urlStr + ": " + e.getMessage());
}
finally
{
try
{
if
(reader !=
null
)
reader.close();
}
catch
(IOException e) {
}
}
return
null
;
}
}
|
XMLUtil4jdom类如下:
package
com.demodashi.pay.util;
import
java.io.ByteArrayInputStream;
import
java.io.IOException;
import
java.io.InputStream;
import
java.util.HashMap;
import
java.util.Iterator;
import
java.util.List;
import
java.util.Map;
import
org.jdom.Document;
import
org.jdom.Element;
import
org.jdom.JDOMException;
import
org.jdom.input.SAXBuilder;
public
class
XMLUtil4jdom {
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public
static
Map doXMLParse(String strxml)
throws
JDOMException, IOException {
strxml = strxml.replaceFirst(
"encoding=\".*\""
,
"encoding=\"UTF-8\""
);
if
(
null
== strxml ||
""
.equals(strxml)) {
return
null
;
}
Map
new
HashMap
InputStream in =
new
ByteArrayInputStream(strxml.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 = XMLUtil4jdom.getChildrenText(children);
}
m.put(k, v);
}
//关闭流
in.close();
return
m;
}
/**
* 获取子结点的xml
* @param children
* @return String
*/
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(XMLUtil4jdom.getChildrenText(list));
}
sb.append(value);
sb.append(
""
+ name +
">"
);
}
}
return
sb.toString();
}
}
|
2、支付回调
支付完成后,微信会把相关支付结果和用户信息发送到我们上面指定的那个回调地址,我们需要接收处理,并返回应答。对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)
关于支付回调接口,我们首先要对于支付结果通知的内容进行签名验证,然后根据支付结果进行相应的处理流程即可。
支付回调需要在微信公众号的微信支付里面设置回调地址:
/**
* 微信平台发起的回调方法,
* 调用我们这个系统的这个方法接口,将扫描支付的处理结果告知我们系统
* @throws JDOMException
* @throws Exception
*/
public
void
weixinNotify(HttpServletRequest request, HttpServletResponse response)
throws
JDOMException, Exception{
//读取参数
InputStream inputStream ;
StringBuffer sb =
new
StringBuffer();
inputStream = request.getInputStream();
String s ;
BufferedReader in =
new
BufferedReader(
new
InputStreamReader(inputStream,
"UTF-8"
));
while
((s = in.readLine()) !=
null
){
sb.append(s);
}
in.close();
inputStream.close();
//解析xml成map
Map
new
HashMap
m = XMLUtil4jdom.doXMLParse(sb.toString());
//过滤空 设置 TreeMap
SortedMap
new
TreeMap
Iterator it = m.keySet().iterator();
while
(it.hasNext()) {
String parameter = (String) it.next();
String parameterValue = m.get(parameter);
String v =
""
;
if
(
null
!= parameterValue) {
v = parameterValue.trim();
}
packageParams.put(parameter, v);
}
// 账号信息
String key = PayConfigUtil.API_KEY;
//key
//判断签名是否正确
if
(PayToolUtil.isTenpaySign(
"UTF-8"
, packageParams,key)) {
//------------------------------
//处理业务开始
//------------------------------
String resXml =
""
;
if
(
"SUCCESS"
.equals((String)packageParams.get(
"result_code"
))){
// 这里是支付成功
//////////执行自己的业务逻辑////////////////
String mch_id = (String)packageParams.get(
"mch_id"
);
String openid = (String)packageParams.get(
"openid"
);
String is_subscribe = (String)packageParams.get(
"is_subscribe"
);
String out_trade_no = (String)packageParams.get(
"out_trade_no"
);
String total_fee = (String)packageParams.get(
"total_fee"
);
//////////执行自己的业务逻辑////////////////
//暂时使用最简单的业务逻辑来处理:只是将业务处理结果保存到session中
//(根据自己的实际业务逻辑来调整,很多时候,我们会操作业务表,将返回成功的状态保留下来)
request.getSession().setAttribute(
"_PAY_RESULT"
,
"OK"
);
System.out.println(
"支付成功"
);
//通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
resXml =
"
+
"
+
"
+
" "
;
}
else
{
resXml =
"
+
"
+
"
+
" "
;
}
//------------------------------
//处理业务完毕
//------------------------------
BufferedOutputStream out =
new
BufferedOutputStream(
response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
}
else
{
System.out.println(
"通知签名验证失败"
);
}
}
|
3、支付后网页自动跳转
web页面弹出二维码后,就开启轮询,询问系统后台支付有微信平台的成功支付返回了,如果有,则跳转到支付成功的页面。
<%@ page language="java" pageEncoding="UTF-8"%>
<
html
>
<
head
>
<
meta
http-equiv
=
"Content-Type"
content
=
"text/html; charset=utf-8"
/>
<
script
type
=
"text/javascript"
charset
=
"utf-8"
src
=
"/resource/js/jquery-2.min.js"
>
script
>
<
script
type
=
"text/javascript"
charset
=
"utf-8"
src
=
"/resource/js/layer/layer.js"
>
script
>
<
title
>微信扫码支付例子
title
>
head
>
<
body
>
<
form
id
=
"pay_form"
method
=
"post"
>
<
h1
>可乐特价:0.1元/罐 <
input
id
=
"pay_submit"
name
=
"but"
type
=
"button"
value
=
"微信支付"
/>
h1
>
form
>
body
>
<
script
>
$(function(){
$("#pay_submit").click(function(){
buy('001');//传入可乐的ID号
});
});
/**
* 购买
*/
function buy(productId){
//打开付费二维码 -- 微信二维码
layer.open({
area: ['300px', '300px'],
type: 2,
closeBtn: false,
title: false,
shift: 2,
shadeClose: true,
content:'../user/qrcode.do?productId=' + productId
});
//重复执行某个方法
var t1 = window.setInterval("getPayState('" + productId + "')",1500);
}
function getPayState(productId){
var url = '../user/hadPay.do?productId=' + productId;
//轮询是否已经付费
$.ajax({
type:'post',
url:url,
data:{productId:productId},
cache:false,
async:true,
success:function(json){
if(json.result == 0){
location.href = '/result.jsp';
}
},
error:function(){
layer.msg("执行错误!", 8);
}
});
}
script
>
html
>
|
三、运行效果
项目导入eclipse后,发表到tomcat中运行,或者通过jetty运行,跑起来后,访问:
点击微信支付:
这个时候在手机上用微信扫码:
支付成功后:
然后web网页会跳转到购买成功的页面,这里需要注意,微信支付回调接口,最好部署在公网的服务器上,这样能被回调,我本地使用改hosts的方法来让支付回调,不成功。
四、注意点
本例子为了演示,所以一些业务逻辑特别简单,例如:订单号的生产,这里只是简单的用当前时间long数字来表示:
String out_trade_no =
""
+ System.currentTimeMillis();
//订单号 (调整为自己的生产逻辑)
|
实际开发的时候需要考虑并且情况下的订单号的唯一性。
还有,回调接口,考虑很简单:
//////////执行自己的业务逻辑////////////////
//暂时使用最简单的业务逻辑来处理:只是将业务处理结果保存到session中
//(根据自己的实际业务逻辑来调整,很多时候,我们会操作业务表,将返回成功的状态保留下来)
request.getSession().setAttribute(
"_PAY_RESULT"
,
"OK"
);
System.out.println(
"支付成功"
);
|
实际开发,要把支付成功DB保存下来,以及回调信息log下来等等