前言
今天是2020-4-1愚人节,好久没写博客了,今天准备写一篇微信公众号支付,刚好公司给了我账户,让我有参数测试,由于以前对于支付是小白,所以把这个功能打通花了2天,一天8小时,首先看官网文档,其次是其他朋友写的博客,总算流程走通了,以前这种代码稍微多点的功能,我都是直接把代码往git上放的,因为组织语言能力极差,所以很少写大篇幅的博客,今天可能是愚人节的缘故吧【由于工作原因,有可能一次写不完,我尽量写详细】
环境准备
首先得准备一个域名地址,外网能够访问你本地,我是在natapp上买的【这块不懂的朋友去百度】,假设我申请的域名地址是http://nat200.top
这里的环境准备就是4个参数 appId(公众号id),appSecret(公众号Secret),mchId(商户id),mchKey(api秘钥)
以及支付授权目录和网页授权域名设置
商户中心
登录地址 https://pay.weixin.qq.com
mchId: 这个直接可以获取,不多说
mchKey:这个是设置一个秘钥: 账户中心--账户设置---api秘钥设置
提供一种获取方式: https://suijimimashengcheng.51240.com/
支付授权目录添加
产品中心--> 开发配置 --> JSAPI支付
如何设置呢?我刚开始看文档的时候,以为是 微信内H5调起支付 页面的上一级,但是设置了不生效,后面换的是后端接口的地址
http://nat200.top/wxJsapi/
前面的是我域名,wxJsapi是我controller类上面的路径,提交订单的全路径是 http://nat200.top/wxJsapi/pay 在pay方法里处理完的返回的页面就是 微信内H5调起支付的所在页面
公众号中心
微信公众已认证的服务号,并且需要开通微信支付功能,必须企业才有资格申请
登录 https://mp.weixin.qq.com
这里获取2个【appId(公众号id),appSecret(公众号Secret)】参数值不多说,公司哪个申请的,问他就可以了
网页授权域名设置
公众号设置-->功能设置-->网页授权域名
这个地址如何写呢?就2点
1、下载 MP_verify_nsZ8KYsVMz282Qgq.txt 点击设置的时候,在弹出页面的那里 可以下载
2、假设我这里填写的域名地址是 http://nat200.top,那么我访问 http://nat200.top/MP_verify_nsZ8KYsVMz282Qgq.txt 可以调通就可以,所以你自己根据自己的情况去设置
环境大致就这些了,注意不要有权限验证
openID
官网说明
为什么获取openID还单独开个标题去说,因为简单点我们微信支付就是对接统一下单这个接口,这个接口的其他参数都好获取,就这个openID稍微困难点
这个一般是用户在注册的时候,就会获取到openID,保存在用户信息表里,每次调用支付接口时,后端可以根据发起的用户查询数据库获取到openID,但这里为了演示,我把获取openID的代码大概写下,具体项目怎么嵌入,自己根据实际情况去
我们看了文档应该知道,我们首先要获取code,在用code获取openID
首先获取code
我刚开始看的时候,就是不知道这些流程怎么一起整进来,我这里获取openID和支付是分开的,因为是为了走流程,所以先获取一个openID,就可以用这个openID测试支付了
我是直接在后台代码的controller直接定义了一个接口,你只要请求该接口,就会重定向获取code的url地址,这里也可以前端获取,真实情况我们是在用户注册的时候就会把这些做完,直接看接口代码
/**
* 前期测试获取code时使用
*/
@GetMapping(value = "/getCode")
public void getCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// redirect_uri 回调地址,该域名需要公众号验证
String backUrl = "http://nat200.top/wxJsapi/getOpenID";
// 拼接url
String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=你的appID"
+ "&redirect_uri=" + URLEncoder.encode(backUrl)
+ "&response_type=code"
+ "&scope=snsapi_base"
+ "&state=STATE#wechat_redirect";
response.sendRedirect(url);
// snsapi_base(静默授权,用户没有感知) snsapi_userinfo
/*
1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
*/
}
这里要强调的是redirect_uri对应的地址 1、首先要URLEncoder.encode对链接编码 2、此链接获取到code,之后,就会回调至你写的这个地址,我是把他回调到了/getOpenID这个接口中
下面看/getOpenID这个接口代码你就明白了
@GetMapping("/getOpenID")
public String getOpenID(HttpServletRequest request, ModelMap model) throws IOException {
String code = request.getParameter("code");
Map<String,Object> map = accessToken(code);
String openId= map.get("openid").toString();
log.info("获取用户的openId: {}", openId);
model.put("openId",openId);
// 我这里返回的是index.html页面,页面在通过 ${openId}获取到值,在传到后端,你可以不需要管,我只是为了一次性测试流程,你只要知// 道这里openID你就拿到了
return "index";
}
public Map<String,Object> accessToken(String code) throws IOException {
List<Object> list = new ArrayList<Object>();
String url="https://api.weixin.qq.com/sns/oauth2/access_token?appid=你的APPID"
+ "&secret=你的公众号secret"
+ "&code="+code
+ "&grant_type=authorization_code";
HttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost(url);
HttpResponse res = client.execute(post);
Map<String,Object> map = null;
if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
HttpEntity entity = res.getEntity();
String str = EntityUtils.toString(entity, "utf-8");
ObjectMapper mapper=new ObjectMapper();
map = mapper.readValue(str, Map.class);
log.info("获取openID返回结果:{}", JSON.toJSONString(map));
}
return map;
}
获取openID返回的数据格式如下
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE"
}
在这里为止,openID已经有了,下面我开始走支付,我会在下面说下,我测试的整个流程是怎么走的,实际怎么嵌入要看项目的
统一下单
我测试的流程如下,再次强调 ,完全是走流程,没有结合业务
1、 首先调用 /getCode 这个接口
2、 会跳转到 /getOpenID 这个接口
3、 处理完会跳转到 index.html页面
4、 页面输入价格,点击提交
5、 会进入后端 /pay接口 -->
6、 处理完,会跳转至 微信内H5调起支付 所在页面
7、 付款支付,根据状态做出不同操作 --> 完
下面主要是代码
首先把官网的 Java版SDK下载,只用里面的 WXPayXmlUtil.java和WXPayUtil.java 2个工具类,其他的看得太累
忘记贴maven依赖了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
往WXPayUtil.java 加入一个方法【支付回调触发时解析用的】,代码如下,这里直接拷贝网上的
public static String inputStream2String(InputStream inStream, String encoding){
String result = null;
ByteArrayOutputStream outStream = null;
try {
if(inStream != null){
outStream = new ByteArrayOutputStream();
byte[] tempBytes = new byte[1024];
int count = 0;
while((count = inStream.read(tempBytes)) != -1){
outStream.write(tempBytes, 0, count);
}
tempBytes = null;
outStream.flush();
result = new String(outStream.toByteArray(), encoding);
outStream.close();
}
} catch (Exception e) {
result = null;
}
return result;
}
下面看 index.html 的代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf8">
<title>微信支付</title>
</head>
<body>
<h2>微信订单支付</h2>
<form action="http://nat200.top/wxJsapi/pay" method="post">
<div>
<label>商品价格</label>
<input name="amount" value=""/>
</div>
<div>
<label>openId</label>
<input name="openId" th:value="${openId}"/>
</div>
<input type="submit"/>
</form>
</body>
</html>
看了这个,就该看 接口 /pay 的代码了
@PostMapping(value = "/pay")
public String wxPluginSubmit(@RequestParam("amount") Integer amount,@RequestParam("openId")String openId,
HttpServletRequest request, Model model) {
// 1 获取此时的用户信息,从用户信息里面获取到 openId [用户注册的时候直接绑定]
// 2 保存订单信息
String orderNo = System.currentTimeMillis() + "";
// 3 传入订单编号,拼装参数发送请求
// 测试,先写成固定的
try {
model.addAttribute("parameterMap",wxPayService.getParameterMap(orderNo, request,openId,amount));
}catch (BusinessException be){
log.error(be.getMsg());
// 返回error页面
}catch (Exception e){
// 返回error页面
}
return "wxpay";
}
接下来看 wxPayService.getParameterMap 接口实现的代码
@Override
public Map<String, String> getParameterMap(String orderNo, HttpServletRequest request,String openId,Integer amount) {
// 2 生成ip
String ip = IpUtil.getIp(request);
// 4 定义一个集合
Map<String, String> packageParams = new HashMap<>();
packageParams.put("appid", wechatAccountConfig.getAppId());
packageParams.put("mch_id", wechatAccountConfig.getMchId());
packageParams.put("nonce_str", WXPayUtil.generateNonceStr());
packageParams.put("body", Constants.BODY);
packageParams.put("out_trade_no", orderNo);
packageParams.put("notify_url", wechatAccountConfig.getNotifyUrl());// 回调地址
packageParams.put("openid", openId);
packageParams.put("spbill_create_ip", ip);
packageParams.put("sign_type", "MD5");
packageParams.put("total_fee",amount.toString());
packageParams.put("trade_type", "JSAPI"); // 交易类型 JSAPI -JSAPI支付 NATIVE -Native支付 APP -APP支付
// 生成签名sign
String sign;
try {
sign = WXPayUtil.generateSignature(packageParams, wechatAccountConfig.getMchKey());
} catch (Exception e) {
e.printStackTrace();
log.error("生成签名失败: {}" + JSON.toJSONString(packageParams));
throw new BusinessException(ResultEntity.FAIL_CODE,"生成签名失败");
}
packageParams.put("sign", sign);
String prepayXml;
try {
prepayXml = WXPayUtil.mapToXml(packageParams);
} catch (Exception e) {
e.printStackTrace();
log.error("map生成字符串出错: {}" + JSON.toJSONString(packageParams));
throw new BusinessException(ResultEntity.FAIL_CODE,"map转化成字符串出错");
}
// 调用接口获取 预支付id
log.info("发送统一请求接口参数,{}",JSON.toJSONString(packageParams));
String xmlStr = HttpUtil.sendPost(Constants.WXPAY_UNIFIEDORDER_GATEWAY, prepayXml);//发送post请求"统一下单接口
if (null == xmlStr || xmlStr.indexOf(Constants.FAIL) != -1) {
log.error("统一下单返回报文:{}" ,xmlStr);
throw new BusinessException(ResultEntity.FAIL_CODE,"发送统一下单接口异常");
}
Map<String, String> map = null;
try {
map = WXPayUtil.xmlToMap(xmlStr);
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException(ResultEntity.FAIL_CODE,"map转化成字符串出错");
}
// 获取预支付交易会话标识
String prepay_id = map.get("prepay_id");
/**
* 以下内容是返回前端页面的json数据
*/
Map<String, String> payMap = new HashMap<>();
payMap.put("appId", wechatAccountConfig.getAppId());
payMap.put("timeStamp", WXPayUtil.getCurrentTimestamp() + "");
payMap.put("nonceStr", WXPayUtil.generateNonceStr());
payMap.put("signType", "MD5");
payMap.put("package", "prepay_id=" + prepay_id);
String paySign;
try {
paySign = WXPayUtil.generateSignature(payMap, wechatAccountConfig.getMchKey());
} catch (Exception e) {
e.printStackTrace();
log.error("封装前台数据签名时出错: {}", JSON.toJSONString(payMap));
throw new BusinessException(ResultEntity.FAIL_CODE,"封装前台数据签名时出错");
}
payMap.put("paySign", paySign);
// package在jsp属于关键字【但是参与签名的名字必须是 package ,切记切记】,所以改名为packageValue,不过我后续改为 HTML了
payMap.put("packageValue", "prepay_id=" + prepay_id);
// 返回订单号
payMap.put("no", orderNo);
log.info("返回前端的支付数据: {}" , JSON.toJSONString(payMap));
return payMap;
}
这个方法一出,工具类就多了,下面依次贴出来
public class IpUtil {
/**
* 获取ip
*/
public static String getIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = ip.indexOf(",");
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
}
ip = request.getHeader("X-Real-IP");
if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
return ip;
}
return request.getRemoteAddr();
}
}
常量配置类
public class Constants {
/**
* 商品描述
*/
public static String BODY = "";
/**
* 签名类型
*/
public enum SignType {
MD5, HMACSHA256
}
/**
* 签名字段名称
*/
public static final String FIELD_SIGN = "sign";
/**
* 统一下单
*/
public static final String WXPAY_UNIFIEDORDER_GATEWAY = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 查询地址
*/
public static final String WXPAY_ORDERQUERY_GATEWAY = "https://api.mch.weixin.qq.com/pay/orderquery";
/**
* 关闭订单
*/
public static final String WXPAY_CLOSEORDER_GATEWAY = " https://api.mch.weixin.qq.com/pay/closeorder";
/**
* 回复微信的消息
*/
public static final String NOTIFY_RESPONSE_BODY = "<xml>\n" +
" <return_code><![CDATA[SUCCESS]]></return_code>\n" +
" <return_msg><![CDATA[OK]]></return_msg>\n" +
"</xml>";
/**
* 失败
*/
public static final String FAIL = "FAIL";
/**
* 成功
*/
public static final String SUCCESS = "SUCCESS";
}
发送请求的,都是网上拷贝的,拷来拷去,也是为了快点实现流程,也没有管是哪个朋友写的了
@Slf4j
public class HttpUtil {
/**
* 向指定URL发送GET方法的请求
*
* @param url
* 发送请求的URL
* @param param
* 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return URL 所代表远程资源的响应结果
*/
public static String sendGet(String url, String param) {
String result = "";
BufferedReader in = null;
try {
String urlNameString = url + "?" + param;
System.out.println(urlNameString);
URL realUrl = new URL(urlNameString);
// 打开和URL之间的连接
URLConnection connection = realUrl.openConnection();
// 设置通用的请求属性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
Map<String, List<String>> map = connection.getHeaderFields();
// 遍历所有的响应头字段
for (String key : map.keySet()) {
System.out.println(key + "--->" + map.get(key));
}
// 定义 BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
}
// 使用finally块来关闭输入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
/**
* 向指定 URL 发送POST方法的请求
*
* @param url
* 发送请求的 URL
* @param param
* 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return 所代表远程资源的响应结果
*/
public static String sendPost(String url, String param) {
PrintWriter out = null;
BufferedReader in = null;
String result = "";
try {
URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
// 设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
// 获取URLConnection对象对应的输出流
out = new PrintWriter(conn.getOutputStream());
// 发送请求参数
out.print(param);
// flush输出流的缓冲
out.flush();
// 定义BufferedReader输入流来读取URL的响应
in = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
log.error("发送 POST 请求出现异常",e);
e.printStackTrace();
return null;
}
//使用finally块来关闭输出流、输入流
finally{
try{
if(out!=null){
out.close();
}
if(in!=null){
in.close();
}
}
catch(IOException ex){
ex.printStackTrace();
}
}
return result;
}
}
几个重要的参数类
@Data
@ConfigurationProperties(prefix = "wechat")
@Component
public class WechatAccountConfig {
// 这些值要到配置文件里面定义,是springboot就知道的
/**
* 公众账号ID
*/
private String appId;
/**
* 公众号Secret
*/
private String appSecret;
/**
* 商户号
*/
private String mchId;
/**
* 商户密钥
*/
private String mchKey;
/**
* 微信公众号支付异步通知地址
*/
private String notifyUrl;
}
/pay接口用到的都拷贝完了,这个接口处理完会跳转到wxpay.html里面,下面看这里的代码,这里的获取后端的值只是一种笨的方式,当时急于测试通过就没管了,后面想改,环境又变了,就算了,这不是重点,前面用的是jsp,但是同时兼容HTML和jsp还加入了一些配置类,就直接换掉了
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>微信网页支付</title>
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
</head>
<body>
<div hidden>
<span th:text="${parameterMap.appId}" id="appId"></span>
<span th:text="${parameterMap.timeStamp}" id="timeStamp"></span>
<span th:text="${parameterMap.nonceStr}" id="nonceStr"></span>
<span th:text="${parameterMap.packageValue}" id="packageValue"></span>
<span th:text="${parameterMap.paySign}" id="paySign"></span>
<span th:text="${parameterMap.no}" id="no"></span>
</div>
<script type="text/javascript">
function cancel(order) {
$.ajax({
type: "POST",
url: "http://nat200.to/wxJsapi/wxPayClose",
data: {
"orderNo": order
},
success: function (data) {
if(data.code === '200'){
alert("关闭成功");
}
}
});
}
//支付接口
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId" :$("#appId").text(),
"timeStamp" : $("#timeStamp").text(),
"nonceStr" : $("#nonceStr").text(),
"package" : $("#packageValue").text(),
"signType" : "MD5",
"paySign" : $("#paySign").text()
},
function(res){
//使用以下方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
alert("您已经支付成功");
} else if(res.err_msg == "get_brand_wcpay_request:cancel") {
alert("您已经放弃了本次支付");
// 用户点击关闭,调用后端接口
cancel($("#no").text())
} else if(res.err_msg == "get_brand_wcpay_request:fail") {
alert("支付失败");
}
}
);
}
if (typeof(WeixinJSBridge) == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}
</script>
</body>
</html>
顺便说一下,我这里springboot返回到HTML页面的配置,maven加入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
yml配置文件加入
spring:
thymeleaf:
prefix: classpath:/static/ # html页面文件
suffix: .html
cache: false #关闭缓存
html页面放在 main/webapp/static
回调接口
@RequestMapping("/notify")
public void notify(HttpServletRequest request, HttpServletResponse response){
InputStream is = null;
try {
is = request.getInputStream();//获取请求的流信息(这里是微信发的xml格式所有只能使用流来读)
String xml = WXPayUtil.inputStream2String(is, "UTF-8");
Map<String, String> notifyMap = WXPayUtil.xmlToMap(xml);//将微信发的xml转map
log.info("支付回调返回的数据:{}", JSON.toJSONString(notifyMap));
// 验签返回的数据
if (WXPayUtil.generateSignature(notifyMap, wechatAccountConfig.getMchKey()).equals(notifyMap.get("sign")) && wechatAccountConfig.getMchId().equals(notifyMap.get("mch_id"))
&& ("JSAPI".equals(notifyMap.get("trade_type")))) {
if(notifyMap.get("return_code").equals(Constants.SUCCESS)){
log.info("支付回调结果校验成功");
// 如果有必要可以判断金额是否匹配
String orderNo = notifyMap.get("out_trade_no"); //商户订单号
String amount = notifyMap.get("total_fee"); //实际支付的订单金额:单位
// TODO 处理项目逻辑,比如发送消息修改订单状态
//orderStatusSend.delaySend("",2000);
}else {
log.info("支付失败:{}",JSON.toJSONString(notifyMap));
}
response.getWriter().write(Constants.NOTIFY_RESPONSE_BODY);
/*BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
out.write(Constants.NOTIFY_RESPONSE_BODY.getBytes());
out.flush();
out.close();*/
}else{
log.info("支付回调结果校验失败===============================");
}
} catch (Exception e) {
e.printStackTrace();
return false;
}finally {
if(null != is){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
对微信支付也是个小白,所以将就着看吧