前段时间在一个App中接入了微信支付功能,想来也稳定运行快一个月了,回头想想自己接入微信支付时候踩过的坑,决定写一篇自己当初想要搜寻的文章,文章准备分3篇完成。
第一篇:微信支付前期准备
第二篇:微信支付接入客户端部分(以Android为例)
第三篇:微信支付接入服务端部分
关于支付流程在第一篇和第二篇文章中说的很清楚了,如果不了解请回看。后端其实就是按照微信支付开发文档要求,将微信支付的API接口封装起来,然后调用即可。
我们这里聚焦支付这一功能,至于其他像查询订单,关闭订单,退款等功能都差不多,后台总结起来就是3步:
- 生成商户自己的订单,然后调用微信接口生成微信预支付订单,获取预支付订单相关数据
- 接收微信支付结果通知(写一个接口给微信调用)并处理自己的业务,例如改变商户订单状态等。
- 处理前端发送过来的查询支付状态的请求,将支付结果返回给前端。
- -
生成预支付订单
下面是生成订单并调用微信支付生成预付订单并获得订单号的核心代码
{
//商户平台的订单实例
RechargeRecordBean rechargeRecord = new RechargeRecordBean();
//商户自己的订单号(交易类型(3位)+用户ID+时间(10位)+4位随机数)
String orderId = ConstCodeTable.FINANCE_ORDER_TYPE_RECHARGE + uId + new SimpleDateFormat("yyyyMMddHH").format(new Date()) + CommonUtil.getRandomStr(4);
rechargeRecord.setRechargeOrderId(orderId);
rechargeRecord.setPrice((int) (rBean.getRechargeAmount() * 100));
rechargeRecord.setYmbCount(rBean.getYmbCount());
rechargeRecord.setBuyerId(uId);
rechargeRecord.setState(0);
insertRechargeRecord(rechargeRecord);//将生成的订单信息插入数据库中
Map<String, String> getPrePayInfo = WeChatPayUtil.getPrePayId(orderId, rBean.getRechargeAmount() + "");
if (getPrePayInfo != null && "SUCCESS".equals(getPrePayInfo.get("return_code")) && "OK".equals(getPrePayInfo.get("return_msg")))
{
/**统一下单接口返回正常的prepay_id,再按签名规范重新生成签名后,将数据传输给APP。
* 参与签名的字段名为appId,partnerId,prepayId,nonceStr,timeStamp,package。注意:package的值格式为Sign=WXPay**/
Map<String, String> resultMap = new TreeMap<>();
resultMap.put("appid", WeChatConfig.APP_ID);
resultMap.put("partnerid", WeChatConfig.MCH_ID);
resultMap.put("prepayid", getPrePayInfo.get("prepay_id"));
resultMap.put("package", "Sign=WXPay");
resultMap.put("noncestr", WeChatPayUtil.getRandomString());
resultMap.put("timestamp", String.valueOf(System.currentTimeMillis()).toString().substring(0, 10));//本来生成的时间戳为10位
String sign = WeChatPayUtil.getSign(resultMap);
resultMap.put("sign", sign);
resultMap.put("extData",orderId);
body.put("payResult", resultMap);
} else {
status = "1";
code = ErrorCodeTable.FINANCIAL_WECHAT_PREP_ID_ERROR;
}
}
通过上面的代码,客户端就拿到了调起微信支付的所有参数。
接收微信支付通知
当完成支付后,腾讯后台会调用我们设置的接口链接通知我们服务端支付结果。我们将依据支付结果完成业务逻辑处理。
public void weChatPayNotify(HttpServletRequest request, HttpServletResponse response) throws IOException, JDOMException {
//读取参数
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<String, String> m = XMLUtil.doXMLParse(sb.toString());
for (String keyValue : m.keySet()) {
System.out.println(keyValue + "=" + m.get(keyValue));
LOGGER.debug("wechatPay",keyValue + "=" + m.get(keyValue));
}
//过滤空 设置 TreeMap
SortedMap<String, String> packageParams = 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 resXml = "";
if (WeChatPayUtil.isTenpaySign("UTF-8", packageParams))
{
if ("SUCCESS".equals(packageParams.get("result_code"))) {
String mchId = packageParams.get("mch_id"); //商户号
String openid = packageParams.get("openid"); //用户标识
String myRechargeOrderId = packageParams.get("out_trade_no"); //商户订单号
String totalFee = packageParams.get("total_fee");
String transactionId = packageParams.get("transaction_id"); //微信支付订单号
RechargeRecordBean rcgRecordBean = financialMapper.getRcgRecordBean(myRechargeOrderId);
if (!WeChatConfig.MCH_ID.equals(mchId) || rcgRecordBean == null ||
new BigDecimal(totalFee).compareTo(new BigDecimal(rcgRecordBean.getPrice()))!= 0) {
System.out.println("支付失败,错误信息:" + "参数错误");
//这里写你自己的业务逻辑
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[参数错误]]></return_msg>" + "</xml> ";
} else {
//处理微信重复通知,支付和过期的订单直接返回
if (0 != rcgRecordBean.getState()) {
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
System.out.println("重复通知");
} else {
//这里写你自己的业务逻辑
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
System.out.println("成功处理订单");
}
}
} else {
System.out.println("支付失败,错误信息:" + packageParams.get("err_code"));
//这里写你自己的业务逻辑
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
}
} else {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[通知签名验证失败]]></return_msg>" + "</xml> ";
System.out.println("通知签名验证失败");
}
//------------------------------
//处理业务完毕
//------------------------------
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
}
工具类
网络请求类
/**
* Copyright (C) 2018
* 天津圣女果科技完全享有此软件的著作权,违者必究
*
* @author ben
* @version 1.0
* @createDate 2018/1/16 16:34
* @description
*/
public class HttpClientUtil {
/**
* http post 方法
*
* @param url
* @param xmlInfo
* @return
*/
public static String postWithOutSSL(String url, String xmlInfo) {
try {
HttpClient httpclient = new HttpClient();
httpclient.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "UTF-8");
PostMethod httpPost = new PostMethod(url);
httpPost.setRequestEntity(new StringRequestEntity(xmlInfo, "text/xml", "utf-8"));
httpclient.executeMethod(httpPost);
BufferedReader reader = new BufferedReader(new InputStreamReader(httpPost.getResponseBodyAsStream()));
StringBuffer stringBuffer = new StringBuffer();
String str = "";
while ((str = reader.readLine()) != null) {
stringBuffer.append(str);
}
reader.close();
return stringBuffer.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 以HTTPS 方式的post
*
* @param url
* @param xmlInfo
* @return
* @throws Exception
*/
public static String postWithSSL(String url, String xmlInfo, String cretPath, String mrchId) throws Exception {
//选择初始化密钥文件格式
KeyStore keyStore = KeyStore.getInstance("PKCS12");
//得到密钥文件流
FileInputStream instream = new FileInputStream(new File(cretPath));
try {
//用商户的ID 来解读文件
keyStore.load(instream, mrchId.toCharArray());
} catch (CertificateException e) {
e.printStackTrace();
} finally {
instream.close();
}
/* // 相信自己的CA和所有自签名的证书
SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()).build();*/
//用商户的ID 来加载
SSLContext sslcontext = SSLContexts.custom()
.loadKeyMaterial(keyStore, mrchId.toCharArray())
.build();
// 只允许使用TLSv1协议
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,
new String[]{"TLSv1"}, null,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
//用最新的httpclient 加载密钥
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
StringBuffer ret = new StringBuffer();
try {
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(xmlInfo));
CloseableHttpResponse response = httpclient.execute(httpPost);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent()));
String text;
while ((text = bufferedReader.readLine()) != null) {
ret.append(text);
}
}
EntityUtils.consume(entity);
} finally {
response.close();
}
} finally {
httpclient.close();
}
return ret.toString();
}
/**
* 发送 get请求
*/
public void get(String url) {
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
// 创建httpget.
HttpGet httpget = new HttpGet(url);
System.out.println("executing request " + httpget.getURI());
// 执行get请求.
CloseableHttpResponse response = httpclient.execute(httpget);
try {
// 获取响应实体
HttpEntity entity = response.getEntity();
System.out.println("--------------------------------------");
// 打印响应状态
System.out.println(response.getStatusLine());
if (entity != null) {
// 打印响应内容长度
System.out.println("Response content length: " + entity.getContentLength());
// 打印响应内容
System.out.println("Response content: " + EntityUtils.toString(entity));
}
System.out.println("------------------------------------");
} finally {
response.close();
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭连接,释放资源
try {
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
微信支付类
public class WeChatPayUtil {
/**
* 返回状态码
*/
public static final String RETURN_CODE = "return_code";
/**
* 业务结果
*/
public static final String RESULT_CODE = "result_code";
/**
* 返回信息
*/
public static final String RETURN_MSG = "return_msg";
/**
* 预支付交易订单id
*/
public static final String PREPAY_ID = "prepay_id";
/**
* 获得微信预支付单的id
*
* @param orderId 商户自己的订单号
* @param totalFee 总金额 (元)
* @return 微信预支付订单号
*/
public static Map<String, String> getPrePayId(String orderId, String totalFee) {
int priceFen = new BigDecimal(totalFee).multiply(new BigDecimal(100)).intValue();
if (priceFen <= 0) {
Map<String, String> resultMap = new HashMap<>();
resultMap.put("msg", "付款金额错误");
resultMap.put("code", "500");
return resultMap;
}
Map<String, String> reqMap = new TreeMap<>();
reqMap.put("appid", WeChatConfig.APP_ID);
reqMap.put("mch_id", WeChatConfig.MCH_ID);
reqMap.put("nonce_str", getRandomString());
reqMap.put("body", WeChatConfig.GOOD_SUBJECT);
//reqMap.put("detail", WeChatConfig.GOOD_DETAIL); //非必填,商品详细描述
//reqMap.put("attach", "附加数据"); //非必填
reqMap.put("out_trade_no", orderId); //商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一
reqMap.put("total_fee", priceFen + ""); //订单总金额,单位为分
reqMap.put("spbill_create_ip", getHostIp()); //用户端实际ip
//reqMap.put("fee_type", “CNY”); //交易币种
// reqMap.put("time_start", "20091225091010"); //交易起始时间 非必填 预付订单号有效期为2个小时
// reqMap.put("time_expire", "20091227091010"); //交易结束时间 非必填
// reqMap.put("goods_tag", "ymb"); //商品标记 非必填
reqMap.put("notify_url", WeChatConfig.NOTIFY_URL); //微信支付结果通知地址
reqMap.put("trade_type", "APP"); //交易类型
//reqMap.put("limit_pay", "no_credit"); //指定支付方式,no_credit 指定不能使用信用卡支 非必填
reqMap.put("sign", getSign(reqMap));
String reqStr = creatXml(reqMap);
String retStr = HttpClientUtil.postWithOutSSL(WeChatConfig.PrepayUrl, reqStr);
return getInfoByXml(retStr);
}
/**
* 关闭订单
*
* @param orderId 商户自己的订单号
* @return
*/
public static Map<String, String> closeOrder(String orderId) {
Map<String, String> reqMap = new HashMap<String, String>();
reqMap.put("appid", WeChatConfig.APP_ID);
reqMap.put("mch_id", WeChatConfig.MCH_ID);
reqMap.put("nonce_str", getRandomString());
reqMap.put("out_trade_no", orderId);
reqMap.put("sign", getSign(reqMap));
String reqStr = creatXml(reqMap);
String retStr = HttpClientUtil.postWithOutSSL(WeChatConfig.CloseOrderUrl, reqStr);
return getInfoByXml(retStr);
}
/**
* 查询订单
*
* @param orderId 商户自己的订单号
* @return
*/
public static String queryOrder(String orderId) {
Map<String, String> reqMap = new HashMap<String, String>();
reqMap.put("appid", WeChatConfig.APP_ID);
reqMap.put("mch_id", WeChatConfig.MCH_ID);
reqMap.put("nonce_str", getRandomString());
reqMap.put("out_trade_no", orderId); //商户系统内部的订单号,
reqMap.put("sign", getSign(reqMap));
String reqStr = creatXml(reqMap);
String retStr = HttpClientUtil.postWithOutSSL(WeChatConfig.OrderUrl, reqStr);
return retStr;
}
/**
* 退款
*
* @param orderId 商户订单号
* @param refundId 退款单号
* @param totralFee 总金额(分)
* @param refundFee 退款金额(分)
* @param opUserId 操作员ID
* @return
*/
public static Map<String, String> refundWei(String orderId, String refundId, String totralFee, String refundFee, String opUserId) {
Map<String, String> reqMap = new HashMap<String, String>();
reqMap.put("appid", WeChatConfig.APP_ID);
reqMap.put("mch_id", WeChatConfig.MCH_ID);
reqMap.put("nonce_str", getRandomString());
reqMap.put("out_trade_no", orderId); //商户系统内部的订单号,
reqMap.put("out_refund_no", refundId); //商户退款单号
reqMap.put("total_fee", totralFee); //总金额
reqMap.put("refund_fee", refundFee); //退款金额
reqMap.put("op_user_id", opUserId); //操作员
reqMap.put("sign", getSign(reqMap));
String reqStr = creatXml(reqMap);
String retStr = "";
try {
retStr = HttpClientUtil.postWithSSL(WeChatConfig.RefundUrl, reqStr, WeChatConfig.refund_file_path, WeChatConfig.MCH_ID);
} catch (Exception e) {
e.printStackTrace();
return null;
}
return getInfoByXml(retStr);
}
/**
* 退款查询
*
* @param refundId 退款单号
* @return
*/
public static Map<String, String> getRefundWeiInfo(String refundId) {
Map<String, String> reqMap = new HashMap<String, String>();
reqMap.put("appid", WeChatConfig.APP_ID);
reqMap.put("mch_id", WeChatConfig.MCH_ID);
reqMap.put("nonce_str", getRandomString());
reqMap.put("out_refund_no", refundId); //商户退款单号
reqMap.put("sign", getSign(reqMap));
String reqStr = creatXml(reqMap);
String retStr = HttpClientUtil.postWithOutSSL(WeChatConfig.RefundQueryUrl, reqStr);
return getInfoByXml(retStr);
}
/**
* 得到加密值
*
* @param map
* @return
*/
public static String getSign(Map<String, String> map) {
String[] keys = map.keySet().toArray(new String[0]);
Arrays.sort(keys);
StringBuffer reqStr = new StringBuffer();
for (String key : keys) {
String v = map.get(key);
if (v != null && !v.equals("")) {
reqStr.append(key).append("=").append(v).append("&");
}
}
reqStr.append("key").append("=").append(WeChatConfig.APP_SERCRET);
//MD5加密
return MD5.MD5Encode(reqStr.toString(), "utf-8").toUpperCase();
}
/**
* 获得10位的时间戳
* 如果在JAVA上转换为时间要在后面补上三个0
*
* @return
*/
public static String getTenTimes() {
String t = new Date().getTime() + "";
t = t.substring(0, t.length() - 3);
return t;
}
/**
* 随机字符串
*
* @param
* @return
*/
public static String getRandomString() {
int length = 32;
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; ++i) {
int number = random.nextInt(62);//[0,62)
sb.append(str.charAt(number));
}
return sb.toString();
}
/**
* 本地机器的IP
*
* @return
*/
private static String getHostIp() {
String ip = "";
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return ip;
}
/**
* 传入map 生成头为XML的xml字符串,例:<xml><key>123</key></xml>
*
* @param reqMap
* @return
*/
private static String creatXml(Map<String, String> reqMap) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Set es = reqMap.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)) {
sb.append("<" + k + ">" + "<![CDATA[" + v + "]]></" + k + ">");
} else {
sb.append("<" + k + ">" + v + "</" + k + ">");
}
}
sb.append("</xml>");
return sb.toString();
}
/**
* 将XML转换为Map 验证加密算法 然后返回
*
* @param xml
* @return
*/
private static Map<String, String> getInfoByXml(String xml) {
try {
Map<String, String> map = XMLUtil.doXMLParse(xml);
//对返回结果做校验.去除sign 字段再去加密
String retSign = map.get("sign");
map.remove("sign");
String rightSing = getSign(map);
if (rightSing.equals(retSign)) {
return map;
}
} catch (Exception e) {
return null;
}
return null;
}
/**
* 将金额转换成分
*
* @param fee 元格式的
* @return 分
*/
public static int convertYuanToToFen(double fee) {
return (int) (fee * 100);
}
/**
* 校验签名是否正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
*
* @return boolean
*/
public static boolean isTenpaySign(String characterEncoding, SortedMap<String, String> packageParams) {
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=" + WeChatConfig.APP_SERCRET);
//算出摘要
String mysign = MD5.MD5Encode(sb.toString(), characterEncoding).toLowerCase();
String tenpaySign = ((String) packageParams.get("sign")).toLowerCase();
//System.out.println(tenpaySign + " " + mysign);
return tenpaySign.equals(mysign);
}
/**
* 签名
*
* @param characterEncoding 编码格式
* @param parameters 请求参数
* @return
*/
public static String createSign(String characterEncoding, SortedMap<Object, Object> parameters) {
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();
Iterator it = es.iterator();
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=" + WeChatConfig.APP_SERCRET);
String sign = MD5.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
return sign;
}
}
MD5类
import java.security.MessageDigest;
public class MD5
{
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString;
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}
Xml解析类
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
/**
* Copyright (C) 2018
* ben wang完全享有此软件的著作权,违者必究
*
* @author ben
* @version 1.0
* @createDate 2018/1/16 17:38
* @description
*/
public class XMLUtil
{
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map<String,String> doXMLParse(String strxml) throws JDOMException, IOException
{
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if(null == strxml || "".equals(strxml)) {
return null;
}
Map<String,String> m = new HashMap<String,String>();
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();
}
}
未完待续