一:开发文档
H5支付是指商户在微信客户端外的移动端网页展示商品或服务,用户在前述页面确认使用微信支付时,商户发起本服务唤起微信客户端进行支付。
登录商户平台–>产品中心–>我的产品–>支付产品–>H5支付
注意:需要开通H5支付,并且做一些配置
微信官方体验链接:http://wxpay.wxutil.com/mch/pay/h5.v2.php,请在微信外浏览器打开。
1、用户在商户侧完成下单,使用微信支付进行支付
2、由商户后台向微信支付发起下单请求(调用统一下单接口)注:交易类型trade_type=MWEB
3、统一下单接口返回支付相关参数给商户后台,如支付跳转url(参数名“mweb_url”),商户通过mweb_url调起微信支付中间页
4、中间页进行H5权限的校验,安全性检查(此处常见错误请见下文)
5、如支付成功,商户后台会接收到微信侧的异步通知
6、用户在微信支付收银台完成支付或取消支付,返回商户页面(默认为返回支付发起页面)
7、商户在展示页面,引导用户主动发起支付结果的查询
8、商户后台判断是否接到收微信侧的支付结果通知,如没有,后台调用我们的订单查询接口确认订单状态
9、展示最终的订单支付结果给用户
H5支付文档
二:集成步骤
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
dependency>
<dependency>
<groupId>dom4jgroupId>
<artifactId>dom4jartifactId>
dependency>
1、再resource/config下增加配置文件wechatConfig.properties
2、properties文件内容如下:
###############WeChatPay start#################
wechat.appid=wxd123456778902
wechat.merchantId=1234567890
wechat.merchantName=测试H5支付账号
wechat.merchantKey=12345678901234567890123456789012
wechat.notifyUrl=weChatPayResult
wechat.unifiedorder=https://api.mch.weixin.qq.com/pay/unifiedorder
wechat.orderquery=https://api.mch.weixin.qq.com/pay/orderquery
wechat.accessTocken=https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey
###############WeChatPay end#################
package com.will.wang.wechatPay.utils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
@Component
@PropertySource(value = "classpath:config/wechatConfig.properties")
@ConfigurationProperties(prefix = "wechat")
public class WeChatPropertyConfig {
public String appid;
public String merchantId;
public String merchantName;
public String merchantKey;
public String notifyUrl;
public String unifiedorder;
public String orderquery;
public String accessTocken;
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getMerchantId() {
return merchantId;
}
public void setMerchantId(String merchantId) {
this.merchantId = merchantId;
}
public String getMerchantName() {
return merchantName;
}
public void setMerchantName(String merchantName) {
this.merchantName = merchantName;
}
public String getMerchantKey() {
return merchantKey;
}
public void setMerchantKey(String merchantKey) {
this.merchantKey = merchantKey;
}
public String getNotifyUrl() {
return notifyUrl;
}
public void setNotifyUrl(String notifyUrl) {
this.notifyUrl = notifyUrl;
}
public String getUnifiedorder() {
return unifiedorder;
}
public void setUnifiedorder(String unifiedorder) {
this.unifiedorder = unifiedorder;
}
public String getOrderquery() {
return orderquery;
}
public void setOrderquery(String orderquery) {
this.orderquery = orderquery;
}
public String getAccessTocken() {
return accessTocken;
}
public void setAccessTocken(String accessTocken) {
this.accessTocken = accessTocken;
}
}
package com.will.wang.wechatPay.utils;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Map;
import java.util.TreeMap;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.will.wang.utils.MD5Util;
import com.will.wang.utils.UUIDUtils;
import com.will.wang.utils.XMLUtils;
@Component
public class WeChatUtils {
//支付状态
public static final String SUCCESS= "SUCCESS";
public static final String FAIL= "FAIL";
@Autowired
private WeChatPropertyConfig weChatPropertyConfig;
@Autowired
private XMLUtils XMLUtils;
public String packageOrderQueryXml(Map
TreeMap
treeMap.put("appid", String.valueOf(map.get("appid")));
treeMap.put("mch_id", String.valueOf(map.get("mch_id")));
treeMap.put("transaction_id",String.valueOf(map.get("transaction_id")));
treeMap.put("nonce_str", UUIDUtils.createUUID());
// treeMap.put("out_trade_no", this.out_trade_no);
StringBuilder sb = new StringBuilder();
for (String key : treeMap.keySet()) {
sb.append(key).append("=").append(treeMap.get(key)).append("&");
}
sb.append("key=" + weChatPropertyConfig.getMerchantKey());
String sign = MD5Util.MD5Encode(sb.toString(), "utf-8").toUpperCase();
treeMap.put("sign", sign);
String xmlStr = XMLUtils.doMapToXML(treeMap);
return xmlStr;
}
/**
* 封装需要提交给微信的参数
* 此处参数再实际使用中最好传入一个订单类,根据订单参数封装提交给微信的数据
* @param bill_create_ip
* @param amount
* @return
*/
public String packageParam(String bill_create_ip, Double amount) {
String prePayXml = null;
try {
//订单id
String out_trade_no = UUIDUtils.createUUID();
//获取配置信息,appId,商户id,商户名称,商户key,回调地址
String appid = weChatPropertyConfig.getAppid();
String merchantId = weChatPropertyConfig.getMerchantId();
String merchantKey = weChatPropertyConfig.getMerchantKey();
String merchantName = weChatPropertyConfig.getMerchantName();
String notifyUrl = weChatPropertyConfig.getNotifyUrl();
TreeMap
treeMap.put("appid", appid);
treeMap.put("mch_id", merchantId);// 设置商户号
treeMap.put("nonce_str", UUIDUtils.createUUID());//随机数
treeMap.put("body", URLEncoder.encode("测试支付", "UTF-8"));//商品描述
treeMap.put("out_trade_no", out_trade_no);//商户系统内部的订单号,32个字符内、可包含字母,确保在商户系统唯一,详细说明
// treeMap.put("total_fee", String.valueOf((int)Math.floor(amount*100)));// 商品总金额,以分为单位,古放大100倍向下取整、
treeMap.put("total_fee", String.valueOf(amount));
treeMap.put("spbill_create_ip", bill_create_ip);
treeMap.put("notify_url", notifyUrl);//通知回调地址
treeMap.put("trade_type", "MWEB");//H5支付
String sceneinfo = "{\"h5_info\":{\"type\":\"Wap\",\"wap_url\":\"http://test4.csservice.cn\",\"wap_name\":\"dangfeijiaona\"}}";
treeMap.put("scene_info",sceneinfo);
treeMap.put("openid", null);
treeMap.put("attach", URLEncoder.encode("测试微信支付", "UTF-8"));
StringBuilder sb = new StringBuilder();
for (String key : treeMap.keySet()) {
sb.append(key).append("=").append(treeMap.get(key)).append("&");
}
sb.append("key=" + merchantKey);
String sign = MD5Util.MD5Encode(sb.toString(), "utf-8").toUpperCase();
treeMap.put("sign", sign);
StringBuilder xml = new StringBuilder();
xml.append("
for (Map.Entry
xml.append("<" + entry.getKey() + ">").append(entry.getValue()).append("" + entry.getKey() + ">\n");
}
xml.append("");
System.out.println(xml.toString());
prePayXml = new String(xml.toString().getBytes("UTF-8"), "ISO-8859-1");
//调用微信接口
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return prePayXml;
}
/**
* 获取微信回调数据
* @param request
* @return
*/
public Map
Map
try {
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 = XMLUtils.doXMLParse(result);
}catch(Exception e) {
e.printStackTrace();
}
return map;
}
}
package com.will.wang.utils.http;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;
@Component
public class HttpClient {
public static final int SUCCESS = 200;
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
private static class HttpClientHolder{
private static HttpClient instance=new HttpClient();
}
private HttpClient(){
}
public static HttpClient getInstance(){
return HttpClientHolder.instance;
}
public String doGet(String uri){
HttpGet httpGet = new HttpGet(uri);
return sendHttpGet(httpGet);
}
/**
* 发送Get请求
* @param uri
* @param map
* @return
*/
public String doGet(String uri, Map
List
for(Map.Entry
parameters.add(new BasicNameValuePair(entry.getKey(),String.valueOf(entry.getValue())));
}
HttpGet httpGet = new HttpGet(uri);
String param = null;
try{
param = EntityUtils.toString(new UrlEncodedFormEntity(parameters));
//build get uri with params
httpGet.setURI(new URIBuilder(httpGet.getURI().toString() + "?" + param).build());
}catch(Exception e){
e.printStackTrace();
}
return sendHttpGet(httpGet);
}
/**
* 无参POST请求
* @param uri
* @return
*/
public CloseableHttpResponse doPost(String uri){
HttpPost httpPost = new HttpPost(uri);
return sendHttpPost(httpPost);
}
/**
* 发送post请求,参数用map接收
* @param url 地址
* @param map 参数
* @return 返回值
*/
public CloseableHttpResponse doPost(String url,Map
HttpPost post = new HttpPost(url);
List
for(Map.Entry
pairs.add(new BasicNameValuePair(entry.getKey(),entry.getValue()));
}
try {
post.setEntity(new UrlEncodedFormEntity(pairs,"UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return sendHttpPost(post);
}
/**
* POST发送xml文件
* @param uri
* @param reqXml
* @return
*/
public CloseableHttpResponse doPost(String uri, String reqXml){
HttpPost httpPost = new HttpPost(uri);
httpPost.addHeader("Content-Type", "application/xml");
StringEntity entity = null;
try{
entity = new StringEntity(reqXml, "UTF-8");
}catch(Exception e){
e.printStackTrace();
}
//http post with xml data
httpPost.setEntity(entity);
return sendHttpPost(httpPost);
}
/**
*
* @param uri
* @param map
* @return
*/
public String doPut(String uri, Map
List
for(Map.Entry
parameters.add(new BasicNameValuePair(entry.getKey(),String.valueOf(entry.getValue())));
}
HttpPut httpPut = new HttpPut(uri);
String param = null;
try{
param = EntityUtils.toString(new UrlEncodedFormEntity(parameters));
httpPut.setURI(new URIBuilder(httpPut.getURI().toString() + "?" + param).build());
}catch(Exception e){
e.printStackTrace();
}
return sendHttpPut(httpPut);
}
private CloseableHttpResponse sendHttpPost(HttpPost httpPost){
// HttpEntity entity = null;
// String responseContent = null;
try{
httpClient = HttpClients.createDefault();
// httpPost.setConfig(config);
response = httpClient.execute(httpPost);
// int status = response.getStatusLine().getStatusCode();
// if(status == 200) {//支付成功
// entity = response.getEntity();
// responseContent = EntityUtils.toString(entity, "UTF-8");
// }
}catch (Exception e) {
e.printStackTrace();
}
return response;
}
private String sendHttpGet(HttpGet httpGet){
// CloseableHttpClient httpClient = null;
// CloseableHttpResponse response = null;
HttpEntity entity = null;
String responseContent = null;
try{
httpClient = HttpClients.createDefault();
// httpGet.setConfig(config);
response = httpClient.execute(httpGet);
entity = response.getEntity();
responseContent = EntityUtils.toString(entity, "UTF-8");
}catch(Exception e){
e.printStackTrace();
}finally{
// try{
// if(response != null)
// response.close();
// if(httpClient != null)
// httpClient.close();
// }catch(IOException e){
// e.printStackTrace();
// }
}
return responseContent;
}
private String sendHttpPut(HttpPut httpPut){
// CloseableHttpClient httpClient = null;
// CloseableHttpResponse response = null;
HttpEntity entity = null;
String responseContent = null;
try{
httpClient = HttpClients.createDefault();
// httpPut.setConfig(config);
response = httpClient.execute(httpPut);
entity = response.getEntity();
responseContent = EntityUtils.toString(entity, "UTF-8");
}catch(Exception e){
e.printStackTrace();
}
return responseContent;
}
public String getRequestResult(CloseableHttpResponse response) {
HttpEntity entity = null;
String responseContent = null;
int status = response.getStatusLine().getStatusCode();
if(status == HttpClient.SUCCESS) {//支付成功
entity = response.getEntity();
try {
responseContent = EntityUtils.toString(entity, "UTF-8");
} catch (ParseException | IOException e) {
e.printStackTrace();
}finally {
try{
if(response != null)
response.close();
if(httpClient !=null)
httpClient.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
return responseContent;
}
}
package com.will.wang.utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.stereotype.Component;
@Component
public class XMLUtils {
/**
* Map转xml字符串
* @param params
* @return
*/
public String doMapToXML(Map
StringBuilder xml = new StringBuilder();
xml.append("
for (Map.Entry
xml.append("<" + entry.getKey() + ">").append(entry.getValue()).append("" + entry.getKey() + ">\n");
}
xml.append("");
return null;
}
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public Map
if(null == strxml || "".equals(strxml)) {
return null;
}
Map
try {
// 创建saxReader对象
SAXReader reader = new SAXReader();
// 通过read方法读取一个文件 转换成Document对象
Document document = reader.read(new ByteArrayInputStream(strxml.getBytes("UTF-8")));
//获取根节点元素对象
Element node = document.getRootElement();
m = parse(node, m);
} catch (Exception e) {
e.printStackTrace();
}
return m;
}
private Map
m.put(node.getName(), node.getTextTrim());
// 当前节点下面子节点迭代器
Iterator
// 遍历
while (it.hasNext()) {
// 获取某个子节点对象
Element e = it.next();
// 对子节点进行遍历
parse(e,m);
}
return m;
}
}
package com.will.wang.wechatPay.controller;
import java.util.HashMap;
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.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import com.will.wang.utils.XMLUtils;
import com.will.wang.wechatPay.service.WeChatPayService;
import com.will.wang.wechatPay.utils.WeChatUtils;
@Controller
@RequestMapping("/weChatPay")
public class WeChatPayController {
@Autowired
private WeChatPayService weChatPayService;
@Autowired
private WeChatUtils weChatUtils;
@Autowired
private XMLUtils XMLUtils;
@RequestMapping("/testWeChatPay")
public String testWeChatPay(HttpServletRequest request) {
Double realAmount = 0.00;
//订单来源ip
String bill_create_ip = request.getRemoteAddr();
String realAmountStr = request.getParameter("realAmount");
if(StringUtils.isEmpty(realAmountStr)) {
realAmount = 0.01;//此处应该返回页面错误信息
}
String mweb_url = weChatPayService.testWeChatPay(bill_create_ip, realAmount);
return mweb_url;
}
@RequestMapping("/weChatPayResult")
public void weChatPayResult(HttpServletRequest request,HttpServletResponse response) {
try {
//将微信的返回参数封装成map
Map
String returnCode = String.valueOf(reqData.get("return_code"));
String resultCode = String.valueOf(reqData.get("result_code"));
if (WeChatUtils.SUCCESS.equals(returnCode) && WeChatUtils.SUCCESS.equals(resultCode)) {
boolean signatureValid = weChatPayService.orderQuery(reqData);
if (signatureValid) {
// TODO 业务处理
Map
responseMap.put("return_code", "SUCCESS");
responseMap.put("return_msg", "OK");
String responseXml = XMLUtils.doMapToXML(responseMap);
response.setContentType("text/xml");
response.getWriter().write(responseXml);
response.flushBuffer();
}
}
}catch(Exception e) {
e.printStackTrace();
}
}
}
package com.will.wang.wechatPay.service.impl;
import java.util.Map;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.will.wang.utils.XMLUtils;
import com.will.wang.utils.http.HttpClient;
import com.will.wang.wechatPay.service.WeChatPayService;
import com.will.wang.wechatPay.utils.WeChatPropertyConfig;
import com.will.wang.wechatPay.utils.WeChatUtils;
@Service
public class WeChatPayServiceImpl implements WeChatPayService {
Log log = LogFactory.getLog(WeChatPayServiceImpl.class);
@Autowired
private WeChatPropertyConfig weChatPropertyConfig;
@Autowired
private HttpClient httpRequest;
@Autowired
private XMLUtils XMLUtils;
@Autowired
private WeChatUtils weChatUtils;
public boolean orderQuery(Map
String xmlBody = weChatUtils.packageOrderQueryXml(reqData);
//查询订单
CloseableHttpResponse response = httpRequest.doPost(weChatPropertyConfig.getOrderquery(), xmlBody);
int status = response.getStatusLine().getStatusCode();
try {
if(status == HttpClient.SUCCESS) {
//开始解析这个返回结果,取到需要的东西
String httpResult = httpRequest.getRequestResult(response);
// 过滤
httpResult = httpResult.replaceAll("", "");
Map
String return_code = String.valueOf(map.get("return_code"));
if("SUCCESS".equals(return_code)) {
//此处添加支付成功后,支付金额和实际订单金额是否等价,防止钓鱼
if (map.get("openid") != null && map.get("trade_type") !=null) {
String total_fee = String.valueOf(map.get("total_fee"));
String order_total_fee = String.valueOf(map.get("total_fee"));
if (Integer.parseInt(order_total_fee) >= Integer.parseInt(total_fee)) {
return true;
}
}
}
}
}catch(Exception e) {
e.printStackTrace();
log.error(e.getMessage());
}
return false;
}
@Override
public String testWeChatPay(String bill_create_ip, Double realAmount) {
String mweb_url = null;
try {
//TODO 产生订单,插入数据库状态为支付中
// 将订单参数封装之后给微信
String prePayXml = weChatUtils.packageParam(bill_create_ip, realAmount);
CloseableHttpResponse response = httpRequest.doPost(weChatPropertyConfig.getUnifiedorder(), prePayXml);
int status = response.getStatusLine().getStatusCode();
if(status == HttpClient.SUCCESS) {
//开始解析这个返回结果,取到需要的东西
String httpResult = httpRequest.getRequestResult(response);
// 过滤
httpResult = httpResult.replaceAll("", "");
Map
String return_code = String.valueOf(map.get("return_code"));
if("SUCCESS".equals(return_code)) {
//解析mweb_url
mweb_url = String.valueOf(map.get("mweb_url"));
mweb_url = mweb_url.replaceAll("&", "&");
System.out.println(httpResult);
}else {
log.error(map.get("return_msg"));
}
}
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
}
return mweb_url;
}
}
三:编写页面
在resource/static下新建js文件夹增加jquery-3.3.1.js
在resource/templates下新增支付页面和回调页面
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset= UTF-8"/>
<title>Titletitle>
head>
<body style="font-size: 30px">
<h3><span th:text="${message}">购买商品:生活费span>h3>
<h3><span th:text="${price}">价格:0.01span>h3>
<h3><span th:text="${num}">数量:10个span>h3>
<button style="width: 100%; height: 60px; alignment: center; background: #b49e8f" onclick="commitOrder()">提交订单button>
<script src="../static/js/jquery-3.3.1.js">script>
<script>
function commitOrder() {
$.ajax({
type: "POST",
url: "http://localhost:8080/weChatPay/testWeChatPay",
data: null,
success: function(data) {
console.log(data);
var redirectUrl = "http://localhost:8080/weChatPay/weChatPayResult";
var mwebUrl = data.mweb_url+"&redirect_url="+encodeURIComponent(redirectUrl);
window.location.href=mwebUrl;
}
})
}
script>
body>
htm>
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Titletitle>
head>
<body>
<h1><span th:text="${message}">微信支付-H5支付成功span>h1>
body>
html>
附录
https://www.codercto.com/a/9497.html
微信支付出现提示:get brand_wcpay_request:fail
支付结果返回:invalid total_fee
微信支付提交的金额是不能带小数点的,且是以分为单位,所以我们系统如果是以元为单位要处理下金额,即先乘以100,再去小数点
String.valueOf((int)Math.floor(amount*100))
注意:
https://github.com/wangweiye/willWangServer
https://download.csdn.net/download/wwy1219787539/10571210