您的位置:首页 > 移动开发 > 微信开发

微信公众号支付开发 --Java

2016-09-06 17:20 495 查看
公众号支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。应用场景有:
◆ 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
◆ 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付
◆ 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付

项目描述:本系统采用SpringMVC 、AngularJs框架进行开发,开发的系统依托于微信,即在微信公众账号内进入商家公众号,并打开相应的支付页面,完成支付。因此需要采用公众号支付。

项目开发流程:
一、微信网页授权,即获取code
    首先前端通过url转到login方法,并携带相应的参数如PayVO类,如totalFee等。而后调用getOauthConnectUrl方法获得拼接的地址,并把拼接后的地址传给前端,在前端转向改链接。
    1.拼接的地址传给前端触发,原因不清楚,后端触发不了,报“跨域”的错误
    2.只能在微信中打开才能获得code,并且打开连接的微信账号必须关注相应的公众号才可以获得
    3. 获得code的url如下图,可以看到有两种方式。应用授权作用域scope取snsapi_base
时,不弹出授权页面,直接跳转,只能获取用户openid;当scope取snsapi_userinfo 时弹出授权页面,可通过openid拿到昵称、性别、所在地,即使在未关注的情况下,只要用户授权,也能获取其信息。





/**
* 获得用户的code,并转向支付页面(pay/pay)
* @return
*/
@RequestMapping("/login")
@ResponseBody
public JsonApi login(PayVO payVO) {
LogUtils.trace("--------------------/pay/login-------------------------------");

String oauthUrl;                 //重定向链接
oauthUrl = PayUtil.getOauthConnectUrl(WechatConts.OauthScope.BASE, payVO.getTotalFee());

LogUtils.trace("oauthUrl:" + oauthUrl);

Map<String, String> map = new HashMap<String, String>() ;
map.put("url", oauthUrl);

return new JsonApi(map);
}


public class PayVO {
private String totalFee;

public String getTotalFee() {
return totalFee;
}

public void setTotalFee(String totalFee) {
this.totalFee = totalFee;
}
}


/**
* 获得授权URL
* @param scope
* @param state
* @return
*/
public static String getOauthConnectUrl(WechatConts.OauthScope scope, String state) {
/**
* 获得访问授权所需要的信息
* 如:appId, Redirect_uri, scope
*/
String appId = WxConfigure.AppId;
String redirectUrl = "http://" + WxConfigure.Redirect_uri;
String connectUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect";
String oauthScope = scope.getScope();

if (state.length() > 100) {
LogUtils.info("stateUrl too Long:" + state);
state = state.substring(0, 100);
}
String stateUrl
4000
= UrlUtil.urlEncode(state);

if (stateUrl.length() >= 128) {
//微信允许的最大state长度是128位
LogUtils.info("stateUrl too Long" + stateUrl);
stateUrl = UrlUtil.urlEncode(state.substring(0, 80));
}
<pre name="code" class="java">public class WxOauthAccessToken {
int errCode;
String errMsg;
String accessToken; //公众号的唯一标识
int expiresIn;
String refreshToken;
String openId;
String scope;

public WxOauthAccessToken(ErrorMng.ErrorCode errorCode) {
this.errCode = errorCode.getErrorID();
this.errMsg = errorCode.getErrorMsg();
}

public WxOauthAccessToken(JSONObject jsonObject) {
if (jsonObject.getInteger("errcode") == null) {
this.accessToken = (String) jsonObject.get("access_token");
this.expiresIn = (int) jsonObject.get("expires_in");
this.refreshToken = (String) jsonObject.get("refresh_token");
this.openId = (String) jsonObject.get("openid");
this.scope = (String) jsonObject.get("scope");
} else {
this.errCode = jsonObject.getInteger("errcode");
this.errMsg = jsonObject.getString("errmsg");
}

}


return connectUrl.replace("APPID", appId) .replace("REDIRECT_URI", UrlUtil.urlEncode(redirectUrl)) .replace("SCOPE", oauthScope) .replace("STATE", stateUrl); }

[b]二、通过code换取网页授权access_token,继而获得openid[/b]
 
   第一步中的redirect_url指向下面的地址,当成功时会携带code和相应的参数跳转到该方法下,在该方法里调用getOauthAccessToken方法获得相应的内容,如WxOauthAccessToken类,get和set方法隐藏掉了。

public class WxOauthAccessToken {
int errCode;
String errMsg;
String accessToken; //公众号的唯一标识
int expiresIn;
String refreshToken;
String openId;
String scope;

public WxOauthAccessToken(ErrorMng.ErrorCode errorCode) {
this.errCode = errorCode.getErrorID();
this.errMsg = errorCode.getErrorMsg();
}

public WxOauthAccessToken(JSONObject jsonObject) {
if (jsonObject.getInteger("errcode") == null) {
this.accessToken = (String) jsonObject.get("access_token");
this.expiresIn = (int) jsonObject.get("expires_in");
this.refreshToken = (String) jsonObject.get("refresh_token");
this.openId = (String) jsonObject.get("openid");
this.scope = (String) jsonObject.get("scope");
} else {
this.errCode = jsonObject.getInteger("errcode");
this.errMsg = jsonObject.getString("errmsg");
}

}


/**
* 付款页面
* @param request
* @param response
* @return
* @throws Exception
*/
@RequestMapping("/callback")
public String pay(HttpServletRequest request, HttpServletResponse response) throws Exception {
LogUtils.trace("-----------------------/pay/callback/--------------------------------");

/**
* 获得code和state字段
*/
String code = request.getParameter("code");
String state = request.getParameter("state");
int total = (int)(Float.parseFloat(state)*100);

/**
* 获取openId
*/
WxOauthAccessToken oauthAccessToken = PayUtil.getOauthAccessToken(code);
String openId = oauthAccessToken.getOpenId();

/**
* 需要第一步引导获得用户的code,才能拿到该用户的accessToken
* @param code
* @return
*/
public static WxOauthAccessToken getOauthAccessToken(String code) {
String oauthUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
try {
JSONObject oauthResult = JsonApi.getJson(oauthUrl.replace("APPID", AccessInfo.AppId.VALUE.getAppId())
.replace("SECRET", AccessInfo.AppSecret.VALUE.getAppSecret())
.replace("CODE", code));
return new WxOauthAccessToken(oauthResult);
} catch (IOException e) {
LogUtils.error("Wechat Get Oauth AccessToken Fail" + e.getStackTrace());
return new WxOauthAccessToken(ErrorMng.ErrorCode.WECHAT_API_INVOKE_FAIL);
}
}


三、执行统一下单接口

     首先初始化统一接口需要的各个必须参数(公众号支付统一下单接口),而后调用JsApiReqUtils类的httpsRequest方法请求微信的统一下单接口。其次将返回的值根据前端JsAPi需要的值(发起一个微信支付请求)进行封装并返回给前端,此时是采取的是url带值的方式。
/**
* 付款页面
* @param request
* @param response
* @return
* @throws Exception
*/
@RequestMapping("/callback")
public String pay(HttpServletRequest request, HttpServletResponse response) throws Exception {
LogUtils.trace("-----------------------/pay/callback/--------------------------------");

/**
* 获得code和state字段
*/
String code = request.getParameter("code");
String state = request.getParameter("state");
int total = (int)(Float.parseFloat(state)*100);

/**
* 获取openId
*/
WxOauthAccessToken oauthAccessToken = PayUtil.getOauthAccessToken(code);
String openId = oauthAccessToken.getOpenId();

/**
* 执行统一下单接口,初始化各参数
*/
Date now = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
String outTradeNo = "NO" + dateFormat.format( now );
String body = "测试";
String nonceStr = StringUtil.generateRandomString(16);
//        String spBillCreateIP = ReqUtils.getRealIp(request);
String spBillCreateIP = "127.0.0.1";

//初始化传入参数
JsApiReqData jsApiReqData =
new JsApiReqData(body,nonceStr,outTradeNo,total,spBillCreateIP, openId);

String info = MobiMessage.JsApiReqData2xml(jsApiReqData).replaceAll("__", "_");
LogUtils.trace(info);

String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
StringBuffer sb = JsApiReqUtils.httpsRequest(url, "POST", info);
if (!"".equals(sb.toString())) {
/**
* 得到预支付ID,即prepay_id
*/
Map<String, String> getMap = MobiMessage.parseXml(new String(sb.toString().getBytes(), "utf-8"));
LogUtils.trace(getMap);
String prepay_id = getMap.get("prepay_id");
LogUtils.trace(prepay_id);

// 生成支付签名,这个签名给微信支付的调用使用
Integer timeStamp = DateUtils.getCurrentTimestamp();
JsApiToJsData jsApiToJsData = new JsApiToJsData(timeStamp, nonceStr,prepay_id);

String redirectUrl = "/#pay?appId=APPID&timeStamp=TIMESTAMP&nonceStr=NONCESTR&prepay_id=PREPAYID&signType=MD5&paySign=SIGN";
redirectUrl = redirectUrl.replace("APPID", jsApiToJsData.getAppid())
.replace("TIMESTAMP", jsApiToJsData.getTimeStamp() + "")
.replace("NONCESTR", jsApiToJsData.getNonce_str())
.replace("PREPAYID", jsApiToJsData.getPrepay_id())
.replace("SIGN", jsApiToJsData.getSign());
LogUtils.trace(redirectUrl);

return "redirect:" + redirectUrl ;
} else {
LogUtils.trace("统一下单失败!");
return "";
}
}

/**
* 请求公众号支付API需要提交的数据
* @author Created by fenghui.
* @date Created on 2016/09/06/9:28
**/
public class JsApiReqData {
//每个字段具体的意思请查看API文档
private String appid = "";
private String mch_id = "";
private String body = "";
private String nonce_str = "";
private String sign = "";
private String notify_url = "";
private String out_trade_no = "";
private String spbill_create_ip = "";
private Integer total_fee = 0;
private String trade_type = "";
private String openid = "";

/**
* @param body   要支付的商品的描述信息,用户会在支付成功页面里看到这个信息
* @param nonceStr  随机字符串,不长于32 位
* @param outTradeNo  商户系统内部的订单号,32个字符内可包含字母, 确保在商户系统唯一
* @param totalFee   订单总金额,单位为“分”,只能整数
* @param spBillCreateIP  订单生成的机器IP
* @param openid  用户标识,trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。
*/
public JsApiReqData(String body,String nonceStr,String outTradeNo,Integer totalFee,String spBillCreateIP,
String openid){

//微信分配的公众号ID(开通公众号之后可以获取到)
setAppid(WxConfigure.AppId);

//微信支付分配的商户号ID(开通公众号的微信支付功能之后可以获取到)
setMch_id(WxConfigure.Mch_id);

//接收微信支付异步通知后的回调地址
setNotify_url(WxConfigure.Notify_url);

//交易类型,取值如下:JSAPI,NATIVE,APP,
setTrade_type(WxConfigure.Trade_type);

//要支付的商品的描述信息,用户会在支付成功页面里看到这个信息
setBody(body);

//商户系统内部的订单号,32个字符内可包含字母, 确保在商户系统唯一
setOut_trade_no(outTradeNo);

//订单总金额,单位为“分”,只能整数
setTotal_fee(totalFee);

//订单生成的机器IP
setSpbill_create_ip(spBillCreateIP);

//随机字符串,不长于32 位
setNonce_str(nonceStr);

//用户标识,trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。
setOpenid(openid);

//根据API给的签名规则进行签名
SortedMap<Object, Object> parameters = new TreeMap<Object, Object>();
parameters.put("appid", appid);
parameters.put("mch_id", mch_id);
parameters.put("nonce_str", nonce_str);
parameters.put("body", body);
parameters.put("out_trade_no", out_trade_no);
parameters.put("notify_url", notify_url);
parameters.put("total_fee", total_fee);
parameters.put("spbill_create_ip", spbill_create_ip);
parameters.put("trade_type", trade_type);
parameters.put("openid", openid);

String sign = DictionarySort.createSign(parameters);
//根据给的签名规则进行签名
setSign(sign);//把签名数据设置到Sign这个属性中

}

public class JsApiToJsData {
//每个字段具体的意思请查看API文档
private String appid =
fa15
"";
private Integer timeStamp = 0;
private String nonce_str = "";
private String sign = "";
private String prepay_id = "";
private String signType = "";

/**
* @param nonceStr  随机字符串,不长于32 位
*/
public JsApiToJsData(Integer timeStamp,String nonceStr,String prepay_id){

//微信分配的公众号ID(开通公众号之后可以获取到)
setAppid(WxConfigure.AppId);

setTimeStamp(timeStamp);

setNonce_str(nonceStr);

setPrepay_id(prepay_id);

setSignType("MD5");

//根据API给的签名规则进行签名
SortedMap<Object,Object> parameters = new TreeMap<Object,Object>();
parameters.put("appId", appid);
parameters.put("timeStamp", timeStamp);
parameters.put("nonceStr", nonceStr);
parameters.put("package", "prepay_id=" + prepay_id);
parameters.put("signType",signType);

//根据给的签名规则进行签名
String sign = DictionarySort.createSign(parameters);
setSign(sign);//把签名数据设置到Sign这个属性中
}


    类StringUtil主要用于产生随机字符串。
public class StringUtil {

public static String generateRandomString() {
return generateRandomString(16);
}

public static String generateRandomString(int length) {
String seekChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
int seekLength = seekChars.length();
String str = "";
for (int i = 0; i < length; i++) {
str += seekChars.charAt((int) (Math.random() * seekLength));
}
return str;
}

public static String generateRandomNumber() {
return generateRandomNumber(6);
}

public static String generateRandomNumber(int length) {
String seekChars = "0123456789";
int seekLength = seekChars.length();
String str = "";
for (int i = 0; i < length; i++) {
str += seekChars.charAt((int) (Math.random() * seekLength));
}
return str;
}
}


    类JsApiReqUtils用于获得网页支付提交用户端ip和触发http请求。
public class JsApiReqUtils {

//获取真实IP
public static String getRealIp(HttpServletRequest request) {
if (request == null) {
LogUtils.error("getRealIp request null");
return "0.0.0.0";
}
String realIp = request.getHeader("X-Real-IP");
LogUtils.trace("realIp:" + realIp);
if (realIp != null && realIp.length() > 0) {
return realIp;
}
realIp = request.getHeader("X-Forwarded-For");
LogUtils.trace("realIp2:" + realIp);
if (realIp != null && realIp.length() > 0) {
return realIp;
}
return request.getRemoteAddr();
}

public static StringBuffer httpsRequest(String requestUrl, String requestMethod, String output)
throws Exception {
HttpURLConnection conn = (HttpURLConnection) new URL(requestUrl).openConnection();
//加入数据
conn.setRequestMethod(requestMethod);
conn.setDoOutput(true);

BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream());
buffOutStr.write(output.getBytes("utf-8"));
buffOutStr.flush();
buffOutStr.close();

//获取输入流
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));

String line = null;
StringBuffer sb = new StringBuffer();
while((line = reader.readLine())!= null){
sb.append(line);
}
return sb;
}

}


    MobiMessage类主要用于把json格式的数据转换为微信需要的xml格式的数据,并把返回值从xml格式解析为map格式。
public class MobiMessage {

public static Map<String,String> xml2map(HttpServletRequest request) throws IOException, DocumentException {
Map<String,String> map = new HashMap<String, String>();
SAXReader reader = new SAXReader();
InputStream inputStream = request.getInputStream();
Document document = reader.read(inputStream);
Element root = document.getRootElement();
List<Element> list = root.elements();
for(Element e:list){
map.put(e.getName(), e.getText());
}
inputStream.close();
return map;
}

//订单转换成xml
public static String JsApiReqData2xml(JsApiReqData jsApiReqData){
/*XStream xStream = new XStream();
xStream.alias("xml",productInfo.getClass());
return xStream.toXML(productInfo);*/
MobiMessage.xstream.alias("xml",jsApiReqData.getClass());
return MobiMessage.xstream.toXML(jsApiReqData);
}

public static String RefundReqData2xml(RefundReqData refundReqData){
/*XStream xStream = new XStream();
xStream.alias("xml",productInfo.getClass());
return xStream.toXML(productInfo);*/
MobiMessage.xstream.alias("xml",refundReqData.getClass());
return MobiMessage.xstream.toXML(refundReqData);
}

public static String class2xml(Object object){

return "";
}
public static Map<String, String> parseXml(String xml) throws Exception {
Map<String, String> map = new HashMap<String, String>();
Document document = DocumentHelper.parseText(xml);
Element root = document.getRootElement();
List<Element> elementList = root.elements();
for (Element e : elementList)
map.put(e.getName(), e.getText());
return map;
}

//扩展xstream,使其支持CDATA块
private static XStream xstream = new XStream(new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点的转换都增加CDATA标记
boolean cdata = true;

//@SuppressWarnings("unchecked")
public void startNode(String name, Class clazz) {
super.startNode(name, clazz);
}

protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write("<![CDATA[");
writer.write(text);
writer.write("]]>");
} else {
writer.write(text);
}
}
};
}
});

}


    DateUtils类主要用于获得当前的时间戳。
public class DateUtils {

public static int minTimestamp = 0; //最小的时间戳
public static int maxTimestamp = 1999999999;//最大的时间戳
public static int offset = 0;

/**
* 按照yyyy-MM-dd HH:mm:ss的格式,日期转字符串
*
* @param date
* @return yyyy-MM-dd HH:mm:ss
*/
public static String date2Str(Date date) {
return date2Str(date, "yyyy-MM-dd HH:mm:ss");
}

/**
* 按照参数format的格式,日期转字符串
*
* @param date
* @param format
* @return
*/
public static String date2Str(Date date, String format) {
if (date != null) {
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.format(date);
} else {
return "";
}
}

public static int getCurrentTimestamp() {
return (int) (System.currentTimeMillis() / 1000 + offset);
}

public static boolean setCurrentTimestamp(int timestamp) {
int systemTimestamp = (int) (System.currentTimeMillis() / 1000);
offset = timestamp - systemTimestamp;
return true;

}
}


    类DictionarySort主要根据微信通过的签名规则用于签名,其中key设置路径:微信商户平台-->账户中心-->API安全-->API密钥-->设置API密钥。

public class DictionarySort {

/**
* 签名算法
* @param parameters
* @return
*/
public static String createSign(SortedMap<Object, Object> parameters) {
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序)
Iterator it = es.iterator();
String key = "1082ae0cd40043df9c4531b597970646";
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
Object v = entry.getValue();
if (null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + key);
LogUtils.trace(sb.toString());
String sign = CryptoUtils.MD5(sb.toString()).toUpperCase();
return sign;
}

}


四、汇款后的通知页面

统一下单的参数notify_url决定了付款后的通知地址,应该把起设为改地址,如

//付款成功后的通知地址
public static final String Notify_url = "***/pay/notification";


注意:此时微信会重复发送通知给应用,因此此时接受通知业务的逻辑必须能够识别是否为第一次通知,不然会重复处理接到的通知。官方文档上说可以给微信返回处理成功的通知,但经过多次试验都没能够解决该问题。因此最后采取在业务逻辑上处理通知,即第一次通知进行处理,其余的通知一律忽视。
/**
* 付款后通知跳轉頁面
* @param request
* @param response
* @return
* @throws IOException
* @throws DocumentException
*/
@RequestMapping("/notification")
@ResponseBody
public JsonApi notification(HttpServletRequest request,HttpServletResponse response) throws IOException, DocumentException {
Map<String, String> map = MobiMessage.xml2map(request);
LogUtils.trace(map);

//根据测试成功后的返回数据来进行业务逻辑操作,比如存数据库等

return new JsonApi();
}

五、设置测试目录
    必须设置公众号支付的测试授权目录和支付授权目录,该目录必须和发起支付请求的页面保持一致,比如发起支付请求的url是http://***/#/pay,只需要把该目录设为http://***/即可。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息