公众号支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。应用场景有:
/**
* 获得用户的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 map = new HashMap() ;
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 = UrlUtil.urlEncode(state);
if (stateUrl.length() >= 128) {
//微信允许的最大state长度是128位
LogUtils.info("stateUrl too Long" + stateUrl);
stateUrl = UrlUtil.urlEncode(state.substring(0, 80));
}
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");
}
}
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);
}
}
/**
* 付款页面
* @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 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
public class JsApiToJsData {
//每个字段具体的意思请查看API文档
private String appid = "";
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 parameters = new TreeMap();
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这个属性中
}
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;
}
}
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;
}
}
public class MobiMessage {
public static Map xml2map(HttpServletRequest request) throws IOException, DocumentException {
Map map = new HashMap();
SAXReader reader = new SAXReader();
InputStream inputStream = request.getInputStream();
Document document = reader.read(inputStream);
Element root = document.getRootElement();
List 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 parseXml(String xml) throws Exception {
Map map = new HashMap();
Document document = DocumentHelper.parseText(xml);
Element root = document.getRootElement();
List 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("");
} else {
writer.write(text);
}
}
};
}
});
}
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;
}
}
public class DictionarySort {
/**
* 签名算法
* @param parameters
* @return
*/
public static String createSign(SortedMap 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;
}
}
//付款成功后的通知地址
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 map = MobiMessage.xml2map(request);
LogUtils.trace(map);
//根据测试成功后的返回数据来进行业务逻辑操作,比如存数据库等
return new JsonApi();
}