项目中使用了微信的统一支付以及扫码支付,记录下学习记录
目录
1、配置wechat4j.properties 文件
话不多说,直接上配置文件代码
#url
wechat.url=
#token
wechat.token=wechatserver
#encodingaeskey
wechat.encodingaeskey=
wechat4j.config=/wechat4j-test.properties
#wechat appid
wechat.appid=xxxxxx
#wechat app secret
wechat.appsecret=xxxxx
wechat.mch.id=xxxx
#wechat mch api secret key, must be 32 length
wechat.mch.key=xxxx
配置文件里面主要使用的参数是wechat.mch.id 和wechat.mch.key 分别需要配置商户的id和key,配置好这个后可以进行下一步了。
2.编写支付工具类
在实际开发中,我是将统一支付、撤单、扫码支付等相关接口写到一个支付工具类中,方便多处地方调用。参考微信的官方文档,将其中需要传递的参数封装成一个实体类,方便具体controller中设定参数,具体参数如下:
private String config = "/wechat4j.properties";
private String body;
private String detail;
private String attach;
private String outTradeNo;
private String userOpenId;
private String notifyUrl;
private String authCode;
get..set 省略
3.统一支付
首先写下我自己实际使用中使用的类名称导入列表,方便用户使用中可以参考下,具体类使用的是哪个包下面的
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdom2.JDOMException;
import org.springframework.stereotype.Service;
import org.sword.wechat4j.common.Config;
import org.sword.wechat4j.pay.PayManager;
import org.sword.wechat4j.pay.protocol.unifiedorder.UnifiedorderRequest;
import org.sword.wechat4j.pay.protocol.unifiedorder.UnifiedorderResponse;
import org.sword.wechat4j.util.RandomStringGenerator;
import com.jfinal.kit.StrKit;
import com.sdhr.shu.bean.extend.BizException;
import com.sdhr.shu.common.Constants;
import net.sf.json.JSONObject;
也不会写什么具体解释了,直接贴代码吧,这个方法主要是统一支付的函数,其中可能用到了一些其他的工具类方法,下面尽量全部写进来。
/**
* 支付入口
*
* @param recordId
* @param payMoney 支付金额
* @param request
* @param notifyUrl 回调url
*/
public Map<String, String> ToPayment(Double payMoney, HttpServletRequest request) throws Exception {
// Integer min =
// Integer.valueOf(ReadPropertiesUtils.getInstance().getProperty("order_pay_wechat_expiretime"));
Integer min = 30;
Date start = new Date();
Calendar date = Calendar.getInstance();
date.setTime(start);
date.set(Calendar.MINUTE, date.get(Calendar.MINUTE) + min);
Date end = date.getTime();
// 支付开始时间
String timeStart = DateUtil.formatDateTime(start, DateFormatter.SDF_YMDHMS4);
// 支付结束时间
String timeExpire = DateUtil.formatDateTime(end, DateFormatter.SDF_YMDHMS4);
// 统一下单
UnifiedorderRequest unifiedorderRequest = new UnifiedorderRequest(config);
unifiedorderRequest.setNonce_str(RandomStringGenerator.generate());// 随机字符串
unifiedorderRequest.setBody(body);// 商品描述
unifiedorderRequest.setDetail(detail);// 商品详情
unifiedorderRequest.setAttach(attach); // 随便写,会原样带回
unifiedorderRequest.setOut_trade_no(outTradeNo); // 我们自己的交易订单号
unifiedorderRequest.setTotal_fee((int) (payMoney * 100)); // 钱,单位是分
unifiedorderRequest.setSpbill_create_ip(Utils.getIpAddr(request));// ip地址
unifiedorderRequest.setTime_start(timeStart);
unifiedorderRequest.setTime_expire(timeExpire);
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
unifiedorderRequest.setTime_start(sdf.format(new Date()));
unifiedorderRequest.setTrade_type("JSAPI");// 交易类型
// 获取支付用户的openId
unifiedorderRequest.setOpenid(userOpenId);
unifiedorderRequest.setNotify_url(notifyUrl);// 通知地址
UnifiedorderResponse unifiedorderResponse = PayManager.unifiedorder(unifiedorderRequest, config);
String prepayId = unifiedorderResponse.getPrepay_id();
if (prepayId == null) {
throw new BizException("E110", Message.getMessage("E110"));
}
String jsParam = null;
if (prepayId != null && prepayId.length() > 10) {
// 生成微信支付参数,此处拼接为完整的JSON格式,符合支付调起传入格式
jsParam = createPackageValue(Config.instance(config).getAppid(), Config.instance(config).getMchKey(),
prepayId);
// 此处可以添加订单的处理逻辑
logger.info("生成的微信调起JS参数为:" + jsParam);
logger.debug("-----ToPayment---------outTradeNo:" + outTradeNo + ",totalMoney:" + payMoney);
}
Map<String, String> map = new HashMap<String, String>();
map.put("prepayId", prepayId);
map.put("jsParam", jsParam);
map.put("outTradeNo", outTradeNo);
return map;
}
3.扫码支付
扫码支付对比统一支付相对参数会简单点,具体代码如下:
public Map<String, String> scanQRCodePay(String payMoney, HttpServletRequest request)
throws JDOMException, IOException {
Integer min = 30;
Date start = new Date();
Calendar date = Calendar.getInstance();
date.setTime(start);
date.set(Calendar.MINUTE, date.get(Calendar.MINUTE) + min);
Date end = date.getTime();
// 支付开始时间
String timeStart = DateUtil.formatDateTime(start, DateFormatter.SDF_YMDHMS4);
// 支付结束时间
String timeExpire = DateUtil.formatDateTime(end, DateFormatter.SDF_YMDHMS4);
SortedMap<String, String> params = new TreeMap<String, String>();
params.put("appid", Config.instance().getAppid());
params.put("mch_id", Config.instance().getMchId());
params.put("nonce_str", RandomStringGenerator.generate());
params.put("body", body);
params.put("attach", attach);
params.put("out_trade_no", outTradeNo);
params.put("total_fee", payMoney);
params.put("spbill_create_ip", Utils.getIpAddr(request));
params.put("auth_code", authCode);
params.put("time_start", timeStart);
params.put("time_expire", timeExpire);
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
params.put("time_start", sdf.format(new Date()));
long curren = System.currentTimeMillis();
curren += 15 * 60 * 1000;
Date dateExpire = new Date(curren);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
params.put("time_expire", dateFormat.format(dateExpire));
String sign = createSign(params, Config.instance(config).getMchKey());
params.put("sign", sign);
String httpRequest = httpRequest(Constants.MICROPAYURI, "POST", toXml(params));
Map<String, String> xmlMap = XMLUtil.doXMLParse(httpRequest);
return xmlMap;
}
4、工具类方法
package com.sdhr.shu.util;
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 java.util.Map.Entry;
import java.util.Set;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import java.io.ByteArrayInputStream;
public class XMLUtil {
/**
* 解析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 m = 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 = XMLUtil.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(XMLUtil.getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
/**
* 获取xml编码字符集
* @param strxml
* @return
* @throws IOException
* @throws JDOMException
*/
public static String getXMLEncoding(String strxml) throws JDOMException, IOException {
InputStream in = HttpClientUtil.String2Inputstream(strxml);
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
in.close();
return (String)doc.getProperty("encoding");
}
/**
* 支付成功,返回微信那服务器
* @param return_code
* @param return_msg
* @return
*/
public static String setXML(String return_code, String return_msg) {
return "<xml><return_code><![CDATA[" + return_code + "]]></return_code><return_msg><![CDATA[" + return_msg + "]]></return_msg></xml>";
}
public static String createXML(Map<String,Object> map){
Set<Entry<String,Object>> set=map.entrySet();
set.iterator();
return null;
}
}
/**
* 创建md5摘要,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
*/
private static String createSign(SortedMap<String, String> packageParams, String AppKey) {
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=" + AppKey);
String sign = Md5Util.MD5Encode(sb.toString(), "UTF-8").toUpperCase();
return sign;
}
/**
* 方法名:byteToHex</br>
* 详述:字符串加密辅助方法 </br>
* 开发人员:souvc </br>
* 创建时间:2016-1-5 </br>
*
* @param hash
* @return 说明返回值含义
* @throws 说明发生此异常的条件
*/
private static String byteToHex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
/**
* 方法名:httpRequest</br>
* 详述:发送http请求</br>
* 开发人员:souvc </br>
* 创建时间:2016-1-5 </br>
*
* @param requestUrl
* @param requestMethod
* @param outputStr
* @return 说明返回值含义
* @throws 说明发生此异常的条件
*/
public static String httpRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
StringBuffer buffer = new StringBuffer();
try {
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
httpUrlConn.setRequestMethod(requestMethod);
if ("GET".equalsIgnoreCase(requestMethod))
httpUrlConn.connect();
if (null != outputStr) {
OutputStream outputStream = httpUrlConn.getOutputStream();
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
httpUrlConn.disconnect();
// jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
ce.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return buffer.toString();
}
/**
* 微信下单map to xml
*
* @param params 参数
* @return {String}
*/
public static String toXml(Map<String, String> params) {
StringBuilder xml = new StringBuilder();
xml.append("<xml>");
for (Entry<String, String> entry : params.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
// 略过空值
if (StrKit.isBlank(value))
continue;
xml.append("<").append(key).append(">");
xml.append(entry.getValue());
xml.append("</").append(key).append(">");
}
xml.append("</xml>");
return xml.toString();
}
5.Controller层调用
实际controller层使用的时候,我使用的方法是使用@Autowried 方法注入了一个微信支付相关bean,直接根据相关需要传递的参数设置好后,调用统一支付接口,或者扫码支付接口。下面举简单例子吧。
需要注意的是,微信是区分为预支付和实际支付两种,预支付就是我们将我们本地产生的金额、第三方订单号、交易详细等相关信息封装好请求微信支付接口,需要设定好回调url,在实际支付的接口中根据返回值做相关本地业务逻辑处理。
@Autowired
private WechatPayUtil wc;
/**
*预支付
**/
public OutputBean prePay(HttpServletRequest request){
。。。 具体业务逻辑处理,返回订单号等信息。。。
wc.setBody("收费");
wc.setDetail("xxx收费");
wc.setNotifyUrl("回调url");
wc.setUserOpenId("用户openid");
wc.setOutTradeNo("订单号");
wc.setAttach("附加数据");
map = wc.ToPayment(“金额”, request);//request 必须是请求url
}
public String realPay(HttpServletRequest request, HttpServletResponse response){
logger.info("--- 微信支付回调 ---");
InputStream inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
String result = new String(outSteam.toByteArray(), "utf-8");
Map<String, String> map = null;
map = XMLUtil.doXMLParse(result); //解析微信回调参数
if (map.get("return_code").equals("SUCCESS")) {
if (map.get("result_code").equals("SUCCESS")) {
//支付成功,相关处理,具体可以参考微信支付开发文档
}
}
}
扫码支付与统一下单比较类似,具体实现方法已经贴出来了,这里不做展示,需要注意的是需要对支付订单结果返回值判断做详细点,因为可能出现问题是,5次内微信是小额免密支付的,但5次后需要输入密码,而且因为网路传输问题,对订单的返回都是有影响的,需要间隔5秒左右(官方推荐)再次查询订单状况,根据实际返回情况做页面显示和具体业务逻辑处理。在这里我贴下我自己的判断逻辑,希望大佬指教。
if(!Utils.isNullOrEmpty(returnCode) && "SUCCESS".equals(returnCode)){
String resultCode = map.get("result_code");
String sign = map.get("sign");
if(!Utils.isNullOrEmpty(resultCode) && "SUCCESS".equals(resultCode)){
logger.info("-- 收款成功 ---");
wxScanPayOutPutBean.setErrorCode("SUCCESS");
wxScanPayOutPutBean.setErrorMsg("付款成功");
flag = true;
}else {
// 通信成功,但支付失败
if("USERPAYING".equals(errorCode) || "BANKERROR".equals(errorCode) || "SYSTEMERROR".equals(errorCode) ){
//间隔5秒查询订单,直至支付超时
// 查询订单
String tradeState = "";
for(int times = 0;times<=6;times++){
Thread.sleep(5000);
OrderqueryRequest orderqueryRequest = new OrderqueryRequest();
orderqueryRequest.setAppid(map.get("appid"));
orderqueryRequest.setMch_id(map.get("mch_id"));
orderqueryRequest.setOut_trade_no(preBillUuid);
orderqueryRequest.setNonce_str(RandomStringGenerator.generate());
orderqueryRequest.setSign(map.get("sign"));
OrderqueryResponse orderqueryResponse = PayManager.orderquery(orderqueryRequest);
System.out.println(orderqueryResponse);
if(!Utils.isNullOrEmpty(orderqueryResponse.getResult_code()) && "SUCCESS".equals(orderqueryResponse.getResult_code())){
if("SUCCESS".equals(orderqueryResponse.getTrade_state())){
wxScanPayOutPutBean.setErrorMsg("付款成功");
flag = true;
break;
}else {
wxScanPayOutPutBean.setErrorCode(orderqueryResponse.getTrade_state());
wxScanPayOutPutBean.setErrorMsg(orderqueryResponse.getTrade_state_desc());
}
}
}
if(Utils.isNullOrEmpty(tradeState) || !"SUCCESS".equals(tradeState)){
//撤销订单
Map<String, String> reverseOrder = wc.reverseOrder(preBillUuid);
if(null != reverseOrder){
if(!Utils.isNullOrEmpty(reverseOrder.get("return_code")) && "SUCCESS".equals(reverseOrder.get("return_code"))){
flag = false;
if(!Utils.isNullOrEmpty(reverseOrder.get("result_code")) && "SUCCESS".equals(reverseOrder.get("result_code"))){
wxScanPayOutPutBean.setResult(2);//支付失败,请重新支付
wxScanPayOutPutBean.setErrorCode("FAIL");
wxScanPayOutPutBean.setErrorMsg("收款失败,订单已被撤销,请重试");
}else if("REVERSE_EXPIRE".equals(reverseOrder.get("result_code"))){
wxScanPayOutPutBean.setResult(2);//支付失败,请重新支付
wxScanPayOutPutBean.setErrorCode("FAIL");
wxScanPayOutPutBean.setErrorMsg("收款失败,订单已过期无法撤销");
}else {
wxScanPayOutPutBean.setResult(5);
wxScanPayOutPutBean.setErrorCode(errorCode);
wxScanPayOutPutBean.setErrorMsg("系统错误");
}
}
}else {
wxScanPayOutPutBean.setResult(2);//支付失败,请重新支付
wxScanPayOutPutBean.setErrorCode("FAIL");
wxScanPayOutPutBean.setErrorMsg("收款失败,请重试");
}
System.err.println(reverseOrder);
}
}else if ("ORDERPAID".equals(errorCode) ) {
// 通知前台已经订单已经支付,或已关闭,不能继续扫码
wxScanPayOutPutBean.setResult(3);
wxScanPayOutPutBean.setErrorCode(errorCode);
wxScanPayOutPutBean.setErrorMsg(err_code_des);
}else if ("AUTHCODEEXPIRE".equals(errorCode) || "NOTENOUGH".equals(errorCode) || "NOTSUPORTCARD".equals(errorCode) || "ORDERREVERSED".equals(errorCode) || "BUYER_MISMATCH".equals(errorCode) || "AUTH_CODE_INVALID".equals(errorCode) || "ORDERCLOSED".equals(errorCode) ) {
// 重新扫码,通知前台错误结果
wxScanPayOutPutBean.setResult(4);
wxScanPayOutPutBean.setErrorCode(errorCode);
wxScanPayOutPutBean.setErrorMsg(err_code_des);
}else {
// 通知前台系统错误,不能扫码
wxScanPayOutPutBean.setResult(5);
wxScanPayOutPutBean.setErrorCode(errorCode);
wxScanPayOutPutBean.setErrorMsg("系统错误");
}
}
6.前台页面处理
首先需要引入微信js
<script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js" ></script>
在首先调用预支付接口产生费用后,将相关参数封装为json格式字符串传递到微信支付接口
var prepayId = thatData['prepayId'];
var jsParam = thatData['jsParam'];
var outTradeNo = thatData['outTradeNo'];
toPay(jsParam);
function toPay(jsParam){
var obj = JSON.parse(jsParam);
WeixinJSBridge.invoke(
'getBrandWCPayRequest',obj,
function(res){
// loading();
WeixinJSBridge.log(res.err_msg);
if(res.err_msg == "get_brand_wcpay_request:ok"){
window.location.href = "xxxxx";//去支付成功页面
}else if(res.err_msg == "get_brand_wcpay_request:cancel"){
window.location.href = "支付取消处理"
}else{
$.toast("支付失败", "forbidden", function() {
window.location.href = "支付失败处理"
});
}
}
);
}
扫码支付配置如下,需要先配置js wx.config相关参数,js必须要提前引入
<script src='https://res.wx.qq.com/open/js/jweixin-1.0.0.js'></script>
<script>
/* $(function(){
$.ajax({
type: "Post",
//方法所在页面和方法名
url: "/shu-wechat-client/park/charge/getWxConfig",
data:{},
dataType: "json",
success: function(data) { */
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: "${sessionScope.appId}", // 必填,公众号的唯一标识
timestamp: "${sessionScope.timestamp}", // 必填,生成签名的时间戳
nonceStr: "${sessionScope.nonceStr}", // 必填,生成签名的随机串
signature: "${sessionScope.signature}",// 必填,签名,见附录1
jsApiList: ['checkJsApi','scanQRCode'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});
/* },
error: function(err) {
Lalert("请求出错");
}
});
}) */
</script>
实际扫码收款操作
wx.ready(function() {
wx.checkJsApi({
jsApiList : ['scanQRCode'],
success : function(res) {
}
});
});
//var result ;
$("#scanQrcode").click(function(){
$("#payimg").attr("src","/shu-wechat-client/img/parkpay/nopay.png");
$(".pay-state-word").text("支付中");
wx.scanQRCode({
needResult : 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
scanType : [ "qrCode", "barCode" ], // 可以指定扫二维码还是一维码,默认二者都有
success : function(res) {
var result = res.resultStr; // 当needResult 为 1 时,扫码返回的结果
loading("支付中...");
scanToPayFuction(result); //调用后台实际扫码接口,并根据返回值做处理
}
});
});
7.总结
之上就是我自己做微信统一支付以及扫码支付的一个做法吧,在这里记录下,以后遇到也能够有解决的回溯,也希望能够帮助到一些需要这些知识的人,可能写的不太好,或者有些方法不是很完善,希望大佬指点下,谢谢。