公司APP目前用到了微信的H5支付功能,这里记录一下实现过程。
这篇记录可能回因为微信商户平台的API的变动而变得不完全正确,但是大体流程时不会错的。
1. 微信H5支付流程
不管我这文档写的多漂亮,咋们还是按照微信官方文档来,下看流程图,了解支付的大概过程:
一定要把这副图看懂再去阅读其他文档就方便了。
简单解释一下就是:
用户再浏览器下单—》传回商家后台—》商家后台保存订单,调用微信统一下单接口将订单提交到微信支付后台—》微信支付后台返回一个“URL”给商户后台—》商户后台将URL以重定向的方式返给浏览器(其他方式也行,只要浏览器能获取到)—》浏览器根据得到的URL拉起微信支付功能—》
支付完成后分成微信APP会做两个事:
1.将支付结果给微信后台的—》微信后台确认支付,调用商户后台的回调接口—》商户后台确认支付完成,完成相关操作
2.返回到浏览器中—》浏览器去商户后台查询是否支付完成—》将支付结果展示给用户
2. 准备工作
- 在开放平台注册并认证APP信息
- 在微信商户平台完成账号的注册和商户号的申请
- 将开放平台和商户平台绑定
- 使用申请的商户号登录商户号管理后台,配置API密钥;配置H5域名(下单网页的域名必须和这个域名一致,否无法下单)
3. 后台代码
下单和回调接口
import com.alibaba.fastjson.JSONObject;
import plugins.pay.wechat.domain.WechatPayRet;
import plugins.pay.wechat.sdk.WXPayXmlUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import java.io.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
@RestController
@Api(tags="H5支付接口")
@RequestMapping("/H5Indent")
public class H5IndentController {
@GetMapping("/addWechatH5")
@ApiOperation(value = "微信H5新增订单", notes = "微信H5新增订单")
public void addWechatH5(HttpServletRequest request, HttpServletResponse response){
String APPID = "开放平台APPID";
String MERID = "商户平台商户号";
String SIGNKEY = "商户平台密钥";
String spbillCreateIp = getIpAddr(request); // 用户ip
String scene_info = "{\"h5_info\": {\"type\":\"Wap\",\"wap_url\": \"https://www.xxx.com/pay/payCallback.html\"," +
"\"wap_name\": \"开放平台APP名字\"}}"; // 网页回调地址,流程图第6步所需的
String tradeType = "MWEB"; // H5支付标记
String MD5 = "MD5"; // 虽然官方文档不是必须参数,但是不送有时候会验签失败
String subject = "会员充值";
String totalFee = "支付金额";
// 随机字符串
String nonce_str= getMessageDigest(String.valueOf(new Random().nextInt(10000)).getBytes());
// 回调地址
String notifyUrl = "商户后台回调接口地址";
// 商户订单id
String indentId = "xxxxxxxxxxx";
//签名数据
StringBuilder sb = new StringBuilder()
.append("appid=").append(APPID)
.append("&body=").append(subject)
.append("&mch_id=").append(MERID)
.append("&nonce_str=").append(nonce_str)
.append("¬ify_url=").append(notifyUrl)
.append("&out_trade_no=").append(indentId)
.append("&scene_info=").append(scene_info)
.append("&sign_type=").append(MD5)
.append("&spbill_create_ip=").append(spbillCreateIp)
.append("&total_fee=").append(totalFee)
.append("&trade_type=MWEB")
.append("&key=").append(SIGNKEY);
//签名MD5加密
String sign = (md5(sb.toString())).toUpperCase(); //"把sb.toString()做MD5操作并且toUpperCase()一下,至于怎么MD5,百度一下或者看官方文档"
//封装xml报文
String xml="<xml>"+
"<appid>"+ APPID+"</appid>"+
"<mch_id>"+ MERID+"</mch_id>"+
"<nonce_str>"+nonce_str+"</nonce_str>"+
"<sign>"+sign+"</sign>"+
"<body>"+subject+"</body>"+//
"<out_trade_no>"+indentId+"</out_trade_no>"+
"<total_fee>"+totalFee+"</total_fee>"+//
"<trade_type>"+tradeType+"</trade_type>"+
"<notify_url>"+notifyUrl+"</notify_url>"+
"<sign_type>MD5</sign_type>"+
"<scene_info>"+scene_info+"</scene_info>"+
"<spbill_create_ip>"+spbillCreateIp+"</spbill_create_ip>"+
"</xml>";
String createOrderURL = "https://api.mch.weixin.qq.com/pay/unifiedorder";//微信统一下单接口
String mweb_url = ""; // 微信后台返回的URL
Map map = new HashMap();
JSONObject result = new JSONObject();
try {
// 下单,取接口地址
map = getMwebUrl(createOrderURL, xml);
String return_code = (String) map.get("return_code");
String return_msg = (String) map.get("return_msg");
System.out.println(map);
if("SUCCESS".equals(return_code) && "OK".equals(return_msg)){
mweb_url = (String) map.get("mweb_url"); // 调微信支付接口地址
}else{
System.out.println("统一支付接口获取预支付订单出错");
}
} catch (Exception e) {
System.out.println("统一支付接口获取预支付订单出错");
}
// TODO 商户后台保存订单信息
// 重定向返给前端跳转URL
response.addHeader("location", mweb_url + "&redirect_url=http%3A%2F%2Fwww.xxx.com/pay/payCallback.html");
response.setStatus(302);
}
// 微信APP收款回调
@PostMapping("/wechatNotify")
public void wechatNotify(HttpServletRequest request, HttpServletResponse response)throws Exception{
//解析数据
WechatPayRet ret = parseRequest(request);
String return_code = ret.getReturn_code();
if(return_code.equals("SUCCESS")){
// 开始进行订单处理,ret.getOut_trade_no()就是商户订单号,delIndent()是业务处理逻辑自己完善
// TODO 处理业务逻辑
String result = delIndent(ret.getOut_trade_no());
System.out.println("----------------------------------------------------------------------交易完成!");
if(result.equals("success")){
//成功之后要应答,让微信别调了。但是还是会有重入的可能,所以必须做好数据锁
echo(response);
}else{
return;
}
}else{
return;
}
}
// 应答微信回调
public static void echo(HttpServletResponse response) throws Exception {
response.setContentType("application/xml");
ServletOutputStream os = response.getOutputStream();
os.print("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");
}
// 解析微信后台返回的数据
public static WechatPayRet parseRequest(HttpServletRequest request) throws Exception {
String xml = readXmlFromRequest(request);
Map map = xmlToMap(xml);
WechatPayRet ret = new WechatPayRet();
ret.setReturn_code((String) map.get("result_code"));
ret.setOut_trade_no((String) map.get("out_trade_no"));
return ret;
}
// XML格式字符串转换为Map
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
throw ex;
}
}
// 日志
public static Logger getLogger() {
Logger logger = LoggerFactory.getLogger("wxpay java sdk");
return logger;
}
// 从request读取xml
public static String readXmlFromRequest(HttpServletRequest request) {
StringBuilder xmlSb = new StringBuilder();
try(
ServletInputStream in = request.getInputStream();
InputStreamReader inputStream = new InputStreamReader(in);
BufferedReader buffer = new BufferedReader(inputStream);
){
String line = null;
while((line=buffer.readLine())!=null){
xmlSb.append(line);
}
} catch (Exception e) {
e.printStackTrace();
}
return xmlSb.toString();
}
// 生成随机字符串
public static String getMessageDigest(byte[] buffer) {
char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
try {
MessageDigest mdTemp = MessageDigest.getInstance("MD5");
mdTemp.update(buffer);
byte[] md = mdTemp.digest();
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
return null;
}
}
// 获取用户ip
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = request.getHeader("x-forwarded-for");
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if(ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")){
//根据网卡取本机配置的IP
InetAddress inet=null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress= inet.getHostAddress();
}
}
//对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if(ipAddress!=null && ipAddress.length()>15){ //"***.***.***.***".length() = 15
if(ipAddress.indexOf(",")>0){
ipAddress = ipAddress.substring(0,ipAddress.indexOf(","));
}
}
return ipAddress;
}
// MD5加密
public static String md5(String key) {
System.out.println(key);
char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
try {
byte[] btInput = key.getBytes("UTF-8");
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 获得密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
String s = new String(str);
s = s.toLowerCase();
return s;
} catch (Exception e) {
return null;
}
}
// 发送请求
public static Map getMwebUrl(String url, String xmlParam){
String jsonStr = null;
HttpClient httpClient = new HttpClient();
Map map = new HashMap();
try {
PostMethod method = null;
RequestEntity reqEntity = new StringRequestEntity(xmlParam,"text/json","UTF-8");
method = new PostMethod(url);
method.setRequestEntity(reqEntity);
method.addRequestHeader("Content-Type","application/json;charset=utf-8");
httpClient.executeMethod(method);
StringBuffer resBodyBuf = new StringBuffer();
byte[] responseBody = new byte[1024];
int readCount = 0;
BufferedInputStream is = new BufferedInputStream(method.getResponseBodyAsStream());
while((readCount = is.read(responseBody,0,responseBody.length))!=-1){
resBodyBuf.append(new String(responseBody,0,readCount,"utf-8"));
}
jsonStr = resBodyBuf.toString();
System.out.println(jsonStr);
map = parseXmlToList(jsonStr);
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
// 将xml转成list
public static Map parseXmlToList(String xml) {
Map retMap = new HashMap();
try {
StringReader read = new StringReader(xml);
// 创建新的输入源SAX 解析器将使用 InputSource 对象来确定如何读取 XML 输入
InputSource source = new InputSource(read);
// 创建一个新的SAXBuilder
SAXBuilder sb = new SAXBuilder();
// 通过输入源构造一个Document
org.jdom.Document doc = sb.build(source);
org.jdom.Element root = doc.getRootElement();// 指向根节点
List<Element> es = root.getChildren();
if (es != null && es.size() != 0) {
for (org.jdom.Element element : es) {
retMap.put(element.getName(), element.getValue());
}
}
} catch (Exception e) {
e.printStackTrace();
}
return retMap;
}
}
微信后台返回对象封装,自行补充Getter和Setter
import com.alibaba.fastjson.annotation.JSONField;
import java.util.Date;
/**
* 微信支付返回信息类
* @date 2018年1月24日
*/
public class WechatPayRet {
//返回状态码
private String return_code;
//返回信息
private String return_msg;
//应用ID
private String appid;
//商户号
private String mch_id;
//设备号
private String device_info;
//随机字符串
private String nonce_str;
//签名
private String sign;
//业务结果
private String result_code;
//错误代码
private String err_code;
//错误代码描述
private String err_des;
//用户标识
private String openid;
//是否关注公众账号
private String is_subscribe;
//交易类型
private String trade_type;
//付款银行
private String bank_type;
//总金额
private int total_fee;
//货币种类
private String fee_type;
//现金支付金额
private int cash_fee;
//现金支付货币类型
private String cash_fee_type;
//代金券金额
private int coupon_fee;
//代金券使用数量
private int coupon_count;
//微信支付订单号
private String transaction_id;
//商户订单号
private String out_trade_no;
//商家数据包
private String attach;
//支付完成时间
@JSONField(format="yyyy-MM-dd HH:mm:ss")
private Date time_end;
/**
* 连接是否成功
* @return
*/
public boolean isContact(){
return "SUCCESS".equals(this.return_code);
}
/**
* 业务是否成功
* @return
*/
public boolean isSuccess(){
if(isContact()){
return "SUCCESS".equals(this.result_code);
}
return false;
}
}
XML工具类
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* 2018/7/3
*/
public final class WXPayXmlUtil {
public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);
return documentBuilderFactory.newDocumentBuilder();
}
public static Document newDocument() throws ParserConfigurationException {
return newDocumentBuilder().newDocument();
}
}
4. 其他文档资料
可能遇到的问题,这些大佬都给出了解决办法
https://www.cnblogs.com/lizhilin2016/p/9001452.html
https://blog.csdn.net/lql15005223252/article/details/83146412
https://blog.csdn.net/u010420435/article/details/79307125