微信支付之App支付

时间:2023-03-09 02:54:16
微信支付之App支付

项目接入微信支付的准备工作:

  1. 注册成为开发者,进行资质认证,这里是需要300元的审核费用的;
  2. 在微信商户平台创建应用,提交等待审核(大致需要5-7个工作日);
  3. 应用审核通过之后,进入应用,开通微信支付,提交审核(大致需要2-3个工作日);
  4. 审核通过之后,微信会给注册的邮箱发送商户号,用户和密码等信息。

接入App支付的业务流程主要如下图所示:

微信支付之App支付

接下来,就描述一下接入项目的详细步骤:

  1. 基础信息配置文件:WeiChartConfig.java
  2. 调用API接口的工具类:WeiChartUtil.java
  3. 对外接口:WeiChartController.java

WeiChartConfig.java

此文件主要配置接入微信支付需要的基础信息,例如:商户号、AppId、密钥、异步通知地址等信息,以及微信支付Api地址等信息。

 import com.erenju.util.GlobleConfig;

 /**
* 微信支付基础信息配置
* 沙箱测试地址前边添加:sandboxnew 例如:https://api.mch.weixin.qq.com/sandboxnew/pay/orderquery
* @author rxn
* @date 2018/04/23
*
*/
public class WeiChartConfig { /**
* 预支付请求接口(统一下单)
*/
public static final String PrePayUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
// public static final String PrePayUrl = "https://api.mch.weixin.qq.com/sandboxnew/pay/unifiedorder"; /**
* 异步通知回调地址 ,外网可访问
*/
public static final String notify_url = GlobleConfig.getProperty("PayNotifyUrl")+"/wxpay/notify.json"; /**
* 查询订单地址
*/
public static final String OrderUrl = "https://api.mch.weixin.qq.com/pay/orderquery";
// public static final String OrderUrl = "https://api.mch.weixin.qq.com/sandboxnew/pay/orderquery"; /**
* 关闭订单地址
*/
public static final String CloseOrderUrl = "https://api.mch.weixin.qq.com/pay/closeorder";
// public static final String CloseOrderUrl = "https://api.mch.weixin.qq.com/sandboxnew/pay/closeorder"; /**
* 申请退款地址
*/
public static final String RefundUrl = "https://api.mch.weixin.qq.com/secapi/pay/refund";
// public static final String RefundUrl = "https://api.mch.weixin.qq.com/sandboxnew/secapi/pay/refund"; /**
* 查询退款地址
*/
public static final String RefundQueryUrl = "https://api.mch.weixin.qq.com/pay/refundquery";
// public static final String RefundQueryUrl = "https://api.mch.weixin.qq.com/sandboxnew/pay/refundquery"; /**
* 商户AppId
*/
public static final String AppId = "wx****************"; /**
* 商户号,10位数字
*/
public static final String MchId = "**********"; /**
* 商户密钥 (32位),正式环境和沙箱环境的商户密钥是不同的
*/
public static final String AppSercret = "*****";//正式
/**
* Api Key(32位)
*/
public static final String ApiKey = "*****"; /**
* 商品描述
*/
public static final String body = "****"; /**
* 退款需要证书文件,证书文件的地址
*/
public static final String refund_file_path = "";
}

WeiChartUtil.java

将业务逻辑和调用Api接口的逻辑分开写了,这样业务逻辑没有那么复杂,还增加了代码的复用率。

主要涉及的API如下图所示:(详细的API,请参考微信支付开放平台https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1)

微信支付之App支付

 package com.xhgx.web.pay.weiChart;

 import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Set; import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element; import com.xhgx.util.HttpClientUtil;
import com.xhgx.util.StringUtil; import common.Logger; /**
* 微信支付调用API接口
* @author rxn
* @date 2018/04/23
*
*/
public class WeiChartUtil { public static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); public static Logger log = Logger.getLogger(WeiChartUtil.class); public static void main(String[] args) { Map map = new HashMap<>();
map.put("mch_id", WeiChartConfig.MchId);
map.put("nonce_str", getRandomString());
String sign = getSign(map);
map.put("sign", sign);
System.out.println(map.get("mch_id"));
System.out.println(map.get("nonce_str"));
System.out.println(sign); String string = HttpClientUtil.sendHttpPost("http://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey", map);
System.out.println(string);
} /**
* 请求获取预支付订单
* @param orderId 订单id
* @param totalFee 总金额(单位:分)
* @return
*/
public static Map<String, Object> getPrepayOrderInfo(String orderId,String totalFee){
Map<String, Object> m = new HashMap<String, Object>();
try {
for(int i=0;i<1;i++){
if(StringUtil.isEmpty(orderId)){
m.put("code", -1);
m.put("message", "订单号不能为空");
break;
}
if(Double.parseDouble(totalFee)<=0){
m.put("code", -1);
m.put("message", "订单金额有误");
break;
} Map<String,String> resultMap = getPreyId(orderId, totalFee, "义龙出行"); System.out.println("resultMap:"+resultMap); String return_code = resultMap.get("return_code");//返回状态码
String return_msg = resultMap.get("return_msg");//返回信息
//签名失败
/*if(!StringUtil.isEmpty(return_msg)){
m.put("code", -1);
m.put("message", return_msg);
break;
}*/ if("SUCCESS".equals(return_code)){
// String appid = resultMap.get("appid");//应用APPID
String mch_id = resultMap.get("mch_id");//商户号
// String device_info = resultMap.get("device_info");//设备号
String nonce_str = resultMap.get("nonce_str");//随机字符串
String sign = resultMap.get("sign");//签名
String result_code = resultMap.get("result_code");//业务结果 if("SUCCESS".equals(result_code)){
String trade_type = resultMap.get("trade_type");//交易类型:APP
String prepay_id = resultMap.get("prepay_id");//预支付交易会话标识,有效期为两个小时 //重新生成sign
Map<String,String> signMap = new HashMap<String,String>();
String timeStamp = getTenTimes();
// signMap.put("appid", appid);
signMap.put("appid", WeiChartConfig.AppId);
signMap.put("partnerid", mch_id);
signMap.put("prepayid", prepay_id);
signMap.put("package", "Sign=WXPay");
signMap.put("noncestr", nonce_str);
signMap.put("timestamp", timeStamp);//时间戳:10位 String newSign = getSign(signMap);
// System.out.println("String:"+newSign);
System.out.println(creatXml(signMap));
m.put("sign", newSign);
// m.put("sign", sign);
m.put("appid", WeiChartConfig.AppId);
m.put("mch_id", mch_id);
m.put("nonce_str", nonce_str);
m.put("trade_type", trade_type);
m.put("prepay_id", prepay_id);
m.put("package", "WXPay");
m.put("timestamp", timeStamp); m.put("code", 0);
m.put("message", "请求成功");
break;
}else{
String err_code = resultMap.get("err_code");//错误代码
String err_code_des = resultMap.get("err_code_des");//错误代码描述 m.put("code", -1);
m.put("err_code", err_code);
m.put("message", err_code_des);
break;
} }else{
m.put("code", -1);
m.put("message", "请求失败");
break;
} }
} catch (Exception e) {
e.printStackTrace();
m.put("code", -1);
m.put("message", "程序异常");
}
return m;
} /**
* 统一下单
* @param orderId 订单id
* @param totalFee 总金额(分)
* @param schoolLabel
* @return
*/
public static Map<String, String> getPreyId(String orderId,String totalFee,String schoolLabel){
Map<String,String> m = new HashMap<String,String>();
m.put("appid", WeiChartConfig.AppId);//应用id
m.put("body", "【"+schoolLabel+"】"+WeiChartConfig.body);//商品描述
m.put("mch_id", WeiChartConfig.MchId);//商户号
m.put("nonce_str", getRandomString());//随机字符串
m.put("notify_url", WeiChartConfig.notify_url);//通知地址
m.put("out_trade_no", orderId);//订单号
m.put("spbill_create_ip", getHostIp());//用户端实际ip
Calendar cal = Calendar.getInstance();
cal.add(Calendar.HOUR_OF_DAY, 2);//有效时间
m.put("time_expire", sdf.format(cal.getTime()));//交易结束时间,非必填
Calendar cal1 = Calendar.getInstance();
m.put("time_start", sdf.format(cal1.getTime()));//交易起始时间,非必填
// m.put("total_fee", totalFee);//总金额
m.put("total_fee", 1+"");//总金额
m.put("trade_type", "APP");//交易类型
m.put("sign", getSign(m));//签名 String reqStr = creatXml(m);
String retStr = HttpClientUtil.postHtpps(WeiChartConfig.PrePayUrl, reqStr); System.out.println("微信调起统一下单接口返回结果:"+retStr);
return getInfoByXml(retStr);
} /**
* 查询订单
* @param orderId
* @return
*/
public static Map<String, String> getOrder(String orderId){
Map<String, String> m = new HashMap<String,String>();
m.put("appid", WeiChartConfig.AppId);
m.put("mch_id", WeiChartConfig.MchId);
m.put("nonce_str", getRandomString());
m.put("out_trade_no", orderId);//商户订单号
m.put("sign", getSign(m)); String reqStr = creatXml(m);
String retStr = HttpClientUtil.postHtpps(WeiChartConfig.OrderUrl, reqStr);
return getInfoByXml(retStr);
} /**
* 关闭订单
* @param orderId
* @return
*/
public static Map<String, String> closeOrder(String orderId){
Map<String, String> m = new HashMap<String,String>();
m.put("appid", WeiChartConfig.AppId);
m.put("mch_id", WeiChartConfig.MchId);
m.put("out_trade_no", orderId);//商户订单号
m.put("nonce_str", getRandomString());
m.put("sign", getSign(m)); String reqStr = creatXml(m);
String retStr = HttpClientUtil.postHtpps(WeiChartConfig.CloseOrderUrl, reqStr);
return getInfoByXml(retStr);
} /**
* 退款
* @param orderId 订单号
* @param refundId 退款单号
* @param totalFee 订单金额(分)
* @param refundFee 退款金额(分)
* @return
*/
public static Map<String, String> refund(String orderId,String refundId,String totalFee,String refundFee){
Map<String, String> m = new HashMap<String,String>();
m.put("appid", WeiChartConfig.AppId);
m.put("mch_id", WeiChartConfig.MchId);
m.put("nonce_str", getRandomString());
m.put("out_trade_no", orderId);//商户订单号
m.put("out_refund_no", refundId);//退款单号
m.put("total_fee", totalFee);//订单金额
m.put("refund_fee", refundFee);//退款金额
m.put("sign", getSign(m)); String reqStr = creatXml(m);
String retStr = "";
try {
retStr = HttpClientUtil.postHttplientNeedSSL(WeiChartConfig.RefundUrl, reqStr, WeiChartConfig.refund_file_path, WeiChartConfig.MchId);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
return getInfoByXml(retStr);
}
/**
* 查询退款
* @param orderId
* @return
*/
public static Map<String, String> getOrderRefundQueryInfo(String orderId){
Map<String,String> m = new HashMap<String,String>();
m.put("appid", WeiChartConfig.AppId);
m.put("mch_id", WeiChartConfig.MchId);
m.put("nonce_str", getRandomString());
m.put("out_trade_no", orderId);//订单号
m.put("sign", getSign(m)); String reqStr = creatXml(m);
String retStr = HttpClientUtil.postHtpps(WeiChartConfig.RefundQueryUrl, reqStr);
return getInfoByXml(retStr);
} /**
* 得到随机字符串
*
* @param length
* @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;
}
/**
* 生成为头为XML的xml字符串,例如:<xml><key>123</key></xml>
* @param reqMap
* @return
*/
public static String creatXml(Map<String, String> reqMap){
Set<String> set = reqMap.keySet();
StringBuffer b = new StringBuffer();
b.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
b.append("<xml>");
for(String key : set){
b.append("<"+key+">").append(reqMap.get(key)).append("</"+key+">");
}
b.append("</xml>");
return b.toString();
} /**
* 生成签名
*
* @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(WeiChartConfig.ApiKey); return getMd5ByOrder(reqStr.toString()).toUpperCase();
} /**
* 是否签名正确,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
* @return boolean
*/
public static boolean isTenpaySign(Map<String, String> map) { String sign = map.get("sign");//获取微信返回的sign,进行比较
map.remove("sign");//生成签名时,去除sign 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(WeiChartConfig.ApiKey); //算出摘要
String mysign = getMd5ByOrder(reqStr.toString()).toLowerCase();
String tenpaySign = sign.toLowerCase(); // System.out.println(tenpaySign + " " + mysign);
return tenpaySign.equals(mysign);
} /**
* 解析xml
*
* @param xmlStr
* @return
*/
public static Map<String, String> getInfoByXml(String xmlStr) {
try {
Map<String, String> m = new HashMap<String, String>();
Document d = DocumentHelper.parseText(xmlStr);
Element root = d.getRootElement();
for (Iterator<?> i = root.elementIterator(); i.hasNext();) {
Element element = (Element) i.next();
String name = element.getName();
if (!element.isTextOnly()) {
// 不是字符串 跳过。确定了微信放回的xml只有根目录
continue;
} else {
m.put(name, element.getTextTrim());
}
}
Map<String,String> map = new HashMap<String,String>();
map.putAll(m);
// 对返回结果做校验.去除sign 字段再去加密
String retSign = m.get("sign");
m.remove("sign");
String rightSing = getSign(m);
if (rightSing.equals(retSign)) {
return map;
}
} catch (DocumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
} /**
* 获取10位的时间戳
* @return
*/
public static String getTenTimes(){
String time = System.currentTimeMillis()+"";
return time.substring(0, time.length()-3);
}
/**
* 将文本转排序后,换成MD5加密后的字符串
* @param s
* @return
* @author Rxn
* @date 2018-05-08
*/
public static String getMd5ByOrder(String s){
MessageDigest md;
StringBuilder sStringBuilder = new StringBuilder();
try {
md = MessageDigest.getInstance("MD5");
md.reset();
try {
md.update(s.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
byte[] digest = md.digest();
sStringBuilder.setLength(0); for (int i = 0; i < digest.length; ++i) {
int b = digest[i] & 255;
if (b < 16) {
sStringBuilder.append('0');
} sStringBuilder.append(Integer.toHexString(b));
} return sStringBuilder.toString().toUpperCase(); } catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} return null;
}
}

WeiChartController.java

这里主要涉及一些业务处理,代码中我主要粘了对外的异步通知接口(主要作用是App支付成功后,会有一个支付成功的返回信息告知商户),其他需要的接口已经在WeiChartUtil.java中处理过了,直接调用即可。

 import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.xhgx.util.StringUtil;
import com.xhgx.web.SpringContextUtil;
import common.Logger; /**
*
* @author rxn
* @data 2018/05/03
*
*/
@Controller
public class WeiChartController{ public static Logger log = Logger.getLogger(WeiChartController.class); public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static SimpleDateFormat ymdhms = new SimpleDateFormat("yyyyMMddHHmmss"); /**
* 异步通知,回调方法
* @param request
* @param response
* @throws IOException
*/
@ResponseBody
@RequestMapping("/wxpay/notify.json")
public static void getNotifyInfo(HttpServletRequest request, HttpServletResponse response) throws IOException{
System.out.println("微信APP异步通知,回调方法");
//读取参数
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(); String reqStr = new String(sb);
//将xml解析成map
Map<String, String> reqMap = WeiChartUtil.getInfoByXml(reqStr); //过滤空
Map<String,String> paramMap = new HashMap<String,String>();
Iterator it = reqMap.keySet().iterator();
while (it.hasNext()) {
String name = (String) it.next();
String value = reqMap.get(name); String v = "";
if(value!=null){
v = value.trim();
}
paramMap.put(name, v);
} String resXml="";//响应数据
//创建一个对象,保存通知信息,可以根据业务需求自己定义
PayNotifyTbl payNotifyTbl = new PayNotifyTbl();
payNotifyTbl.setPay_type(1);//支付类型: 1微信 2支付宝
payNotifyTbl.setCreate_dt(new Date());
//验证签名
if(WeiChartUtil.isTenpaySign(paramMap)){
String return_code = paramMap.get("return_code");//返回状态码
String return_msg = paramMap.get("return_msg");//返回信息
String result_code = paramMap.get("result_code");
String err_code = paramMap.get("err_code");
String err_code_des = paramMap.get("err_code_des"); payNotifyTbl.setReturn_code(return_code);
payNotifyTbl.setReturn_msg(return_msg);
payNotifyTbl.setResult_code(result_code);
payNotifyTbl.setErr_code(err_code);
payNotifyTbl.setErr_code_des(err_code_des); if("SUCCESS".equals(return_code)&&"SUCCESS".equals(result_code)){
String appid = paramMap.get("appid");
String mch_id = paramMap.get("mch_id");
String openid = paramMap.get("openid");
String trade_type = paramMap.get("trade_type");
String total_fee = paramMap.get("total_fee");//单位:分
String transaction_id = paramMap.get("transaction_id");//微信支付订单号
String out_trade_no = paramMap.get("out_trade_no");//商户订单号
String time_end = paramMap.get("time_end");//支付完成时间 payNotifyTbl.setAppid(appid);
payNotifyTbl.setOpenid(openid);
payNotifyTbl.setOrder_id(out_trade_no);
payNotifyTbl.setTrade_type(trade_type);
payNotifyTbl.setTotal_fee(Double.parseDouble(total_fee)/100.00);
payNotifyTbl.setTransaction_id(transaction_id);
try {
payNotifyTbl.setTime_end(sdf.format(ymdhms.parse(time_end)));
} catch (ParseException e) {
e.printStackTrace();
} //业务处理,查询订单
OrderTbl order = orderTblService.get(Integer.parseInt(out_trade_no));
//安全起见,添加金额作比较
if(!WeiChartConfig.MchId.equals(mch_id) || order==null || Double.parseDouble(total_fee)/100!=getIntRound(order.getPrice().doubleValue()){
payNotifyTbl.setPay_result("参数错误");
log.info("支付失败,错误信息:" + "参数错误");
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[参数错误]]></return_msg>" + "</xml> ";
}else{
if(order.getOrder_status()==7){
//逻辑处理,支付完成,修改订单状态
order.setOrder_status(OrderStatus.STEP_08.getNodeId());//订单完成
order.setTotal_price(Double.parseDouble(total_fee)/100);//支付费用
order.setPay_way(1);//微信支付
order.setPay_time(new Date());//支付时间
order = ycOrderTblService.save(order); payNotifyTbl.setPay_result("支付成功");
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
}else{
payNotifyTbl.setPay_result("订单已经被处理");
log.info("订单已经被处理");
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
} } }else{
payNotifyTbl.setPay_result(paramMap.get("return_msg"));
log.info("微信支付异步通知返回的错误信息:"+paramMap.get("return_msg"));
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
} }else{
payNotifyTbl.setPay_result("微信支付异步通知签名验证失败");
log.info("微信支付异步通知签名验证失败");
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[通知签名验证失败]]></return_msg>" + "</xml> ";
} if(payNotifyTblService==null){
payNotifyTblService= (PayNotifyTblService) SpringContextUtil.getBean(PayNotifyTblService.class);
} payNotifyTbl = payNotifyTblService.save(payNotifyTbl);
//返回微信支付系统告知成功接收
BufferedOutputStream out = new BufferedOutputStream(
response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} }

在接入微信支付时,入了很多坑,特别注意:生成签名时传入参数的顺序,不同的顺序会导致生成的签名与微信返回的签名不一致。尤其注意调用统一下单接口后,重新生成签名的顺序,一定要按照appid,partnerid,prepayid,noncestr,timestamp,package的顺序,否则App会调起支付页面失败,报签名异常的错误。另外注意:异步通知地址一定是外网可以访问的,而且和微信平台上配置的异步通知地址一致。