公司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