1. 微信支付开发(JSAPI)
- 原作者:小流至江河()
- 原地址:/article/details/79473743
- 修改编写:fazcube(https://github.com/fazcube)
- 修改时间:2021/1/14
1.1. 获取微信支付四大参数
首先要想支持微信支付,必须拥有两个账号,这两个账号一个不能少:
- 微信公众已认证的服务号,并且需要开通微信支付功能(必须是企业才有资格申请)。
- 微信商户平台账号;
此处是账号模板,请参考:
微信公众平台:账户:con*******om 登录密码 ******
公众APPID:wx15*********a8
APPSECEPT : c210***************892d7
微信商户平台:账户:149**********6742 登录密码:******
商户ID:14******42
API密钥:5d5************b35b
1.2. 配置平台
1.3. 开发流程
这里只展示后端代码
微信支付原理
调用官方文档的“统一下单”接口,之后将微信服务器返回的参数经过加工后,
返回到前端,就OK了。需要凑齐“统一下单”接口的所有参数即可。
所有参数解释请参考:官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
以下是所有参数:
其中我们需要的(必填的)参数有以下11个:
参数 | 备注 |
appid | APPID (已有) |
mch_id | 商户ID (已有) |
nonce_str | 随机字符串 |
sign | 签名 |
body | 所支付的名称 |
out_trade_no | 自己所提供的订单号,需要唯一 |
total_fee | 支付金额 |
spbill_create_ip | IP地址 |
notify_url | 回调地址 |
trade_type | 支付类型 |
openid | 支付人的微信公众号对应的唯一标识 |
需要获取这11个参数然后调用微信的“统一下单”接口就可以了。
在这之前先从官网把公众号支付的sdk下载下来,如图
官网下载链接:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1
将下载的zip文件解压,得到\src\main\java\com\github\wxpay\sdk
里面的java文件放在自己项目当中(注意改一下文件头的导包)
将文件放好之后,我们就要开始取之前的11个值了,具体如下:
- appid APPID (已有)
- mch_id 商户ID (已有)
- nonce_str 随机字符串用WXPayUtil中的generateNonceStr()即可,就是生成UUID的方法;
- sign 签名(最后处理sign签名);
- body 所支付的名称
- out_trade_no 自己后台生成的订单号,只要保证唯一就好:如“20210114000001”
- total_fee 支付金额 单位:分
- spbill_create_ip IP地址 网上很多ip的方法,自己找,此处测试给“127.0.0.1”
- notify_url 回调地址:这是微信支付成功后,微信那边会带着一大堆参数(XML格式)调用这个回调api,地址要公网可以访问。
- trade_type 支付类型 公众号支付此处给“JSAPI”
- openid 支付人的微信公众号对应的唯一标识,每个人的openid在不同的公众号是不一样的
1.3.1. 获取openId
(By fazcube)
在开发微信支付的时候,肯定会有需要一个参数,就是openId,这是一个微信用户在一个小程序(公众号)的唯一标识。
先把需要的工具类准备好
import javax.servlet.http.HttpServletRequest;
/**
* @Description: 获取ip
* @Author: By fazcube
* @Date: 2021/1/13
*/
public class IpUtil {
public static String getIp(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.indexOf(",")!=-1){
String[] ips = ipAddress.split(",");
ipAddress = ips[0].trim();
}
return ipAddress;
}
}
发送请求工具
import org.json.JSONException;
import org.json.JSONObject;
import javax.net.ssl.HttpsURLConnection;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
/**
* @Description: 发送请求类
* @Author: By fazcube
* @Date: 2020/11/17
*/
public class ClientUtil {
private static String readAll(Reader rd) throws IOException {
StringBuilder sb = new StringBuilder();
int cp;
while ((cp = rd.read()) != -1) {
sb.append((char) cp);
}
return sb.toString();
}
/**
* 发送post请求并且获得返回的json数据
* @param url
* @param body
* @return
* @throws IOException
* @throws JSONException
*/
public static JSONObject postRequestFromUrl(String url, String body) throws IOException, JSONException {
URL realUrl = new URL(url);
URLConnection conn = realUrl.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
PrintWriter out = new PrintWriter(conn.getOutputStream());
out.print(body);
out.flush();
InputStream instream = conn.getInputStream();
try {
BufferedReader rd = new BufferedReader(new InputStreamReader(instream, Charset.forName("UTF-8")));
String jsonText = readAll(rd);
JSONObject json = new JSONObject(jsonText);
return json;
} finally {
instream.close();
}
}
/**
* 发送get请求并获得返回的json数据
* @param url
* @return
* @throws IOException
* @throws JSONException
*/
public static JSONObject getRequestFromUrl(String url) throws IOException, JSONException {
URL realUrl = new URL(url);
URLConnection conn = realUrl.openConnection();
InputStream instream = conn.getInputStream();
try {
BufferedReader rd = new BufferedReader(new InputStreamReader(instream, Charset.forName("UTF-8")));
String jsonText = readAll(rd);
JSONObject json = new JSONObject(jsonText);
return json;
} finally {
instream.close();
}
}
/**
* 发送post请求,且body参数为json字符串
* @param url
* @param body
* @return
* @throws IOException
* @throws JSONException
*/
public static JSONObject postOfJson(String url, String body) throws IOException, JSONException {
URL realUrl = new URL(url);
URLConnection conn = realUrl.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setRequestProperty("Content-Type", "application/json");
PrintWriter out = new PrintWriter(conn.getOutputStream());
out.print(body);
out.flush();
InputStream instream = conn.getInputStream();
try {
BufferedReader rd = new BufferedReader(new InputStreamReader(instream, Charset.forName("UTF-8")));
String jsonText = readAll(rd);
JSONObject json = new JSONObject(jsonText);
return json;
} finally {
instream.close();
}
}
/**
* 向指定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(), "UTF-8"));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("发送 POST 请求出现异常!" + e);
e.printStackTrace();
}
// 使用finally块来关闭输出流、输入流
finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
return result;
}
}
获取openId
小程序(公众号)微信用户同意授权,前端将code传到后端,来换取openId
@GetMapping(value = "/getOpenId")
public String getOpenId(@RequestParam(name = "code") String code) throws IOException {
//获取token
String accessToken = getAccess_token();
//如果获取token的时候失败了,会返回空字符串,判断获取token是否成功。
if(accessToken.equals("")){
return "获取token失败!";
}
//根据code获取微信用户openId
//传入appid和app_secret
JSONObject json = JSONObject.parseObject(ClientUtil.getRequestFromUrl(
"https://api.weixin.qq.com/sns/jscode2session" +
"?appid=appid" +
"&secret=appSecret" +
"&js_code=" + code +
"&grant_type=authorization_code").toString());
//这边使用的是alibaba的json依赖包
/*
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
*/
if(json.containsKey("errcode")){
return "获取openid失败!";
}
openId = json.getString("openid");
return openId;
}
/**
* 获取token,这边可以使用redis来设置token的有效时间(自行实现)
* @return
* @throws IOException
*/
public String getAccess_token(){
String access_token;
JSONObject token = new JSONObject();
try{
//根据微信接口获取token
token = JSONObject.parseObject(ClientUtil.getRequestFromUrl(
"https://api.weixin.qq.com/cgi-bin/token" +
"?grant_type=client_credential" +
"&appid=appid" +
"&secret=appSecret").toString());
access_token = token.getString("access_token");
}catch (IOException e){
//获取失败的时候返回空字符串
return "";
}
System.out.println("获取到的access_token------>" + this.access_token);
return access_token;
}
1.3.2. 发送支付请求
接下来写controller层
@RequestMapping("/wxPayment")
public Map<String,String> wxPayment(HttpServletRequest request) throws Exception{
// 获取ip地址,使用的是自定义的获取ip地址方法
String ip = IpUtil.getIp(request);
// 统一下单,以下参数都要自己确认一遍
Map<String,String> data = new HashMap<String,String>();
data.put("appid", "wx91e********544f2e");//商家平台ID
data.put("body", "xxx-套餐购买");//标题
data.put("mch_id","mch_id");//商户ID
data.put("out_trade_no", "20210114000001");//订单号
data.put("nonce_str", WXPayUtil.generateNonceStr());
data.put("total_fee", "1");//金额,单位是分
data.put("spbill_create_ip", ip);
data.put("notify_url", "https://www.xxx.com/wxPay/notify");//微信调取回调
data.put("trade_type", "JSAPI"); // 支付类型
data.put("openid","o6M8m6wuX*******15d0tocwMQ");//用户标识
// 将以上10个参数传入,换取sign签名
String sign = WXPayUtil.generateSignature(data, "api密钥");
// 将sign签名put进去,凑齐11个参数
data.put("sign", sign);
// 将所有参数(map)转xml格式,这里使用的是微信的官方方法
String xml = WXPayUtil.mapToXml(data);
// 统一下单接口 https://api.mch.weixin.qq.com/pay/unifiedorder 这个是固定的使用这个接口
String unifiedOrder_url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
//输出一下xml文件测试一下参数拼接是否正常
System.out.println("xml为:" + xml);
//这边使用的是自定义工具类的发送请求方法
String xmlStr = ClientUtil.sendPost(unifiedOrder_url,xml);
System.out.println("xmlStr为:" + xmlStr);
// **************以下内容是返回前端页面的json数据**************
// 预支付id
String prepay_id = "";
Map<String, String> map = WXPayUtil.xmlToMap(xmlStr);
//判断是否成功
if(map.get("return_code").equals("SUCCESS")){
prepay_id = (String) map.get("prepay_id");
}else {
System.out.println(map.get("return_msg").toString());
}
//将这个6个参数传给前端,固定的六个参数,前端要接收
Map<String, String> payMap = new HashMap<String, String>();
payMap.put("appId", WxData.APP_ID); //appId
payMap.put("timeStamp", WXPayUtil.getCurrentTimestamp() + "");//时间戳
payMap.put("nonceStr", WXPayUtil.generateNonceStr()); //随机字符串
payMap.put("signType", "MD5"); //固定写MD5就好
payMap.put("package", "prepay_id="+prepay_id);//写法就是这样,提交格式如"prepay_id=***"
//还是通过五个参数获取到paySign
String paySign = WXPayUtil.generateSignature(payMap, "api密钥");
//再将paySign这个参数put进去,凑齐六个参数
payMap.put("paySign", paySign);
return payMap;
}
/**
* @Title: callBack
* @Description: 支付完成的回调函数
* @param:
* @return:
*/
@RequestMapping("/notify")
public String callBack(HttpServletRequest request, HttpServletResponse response) {
System.out.println("微信支付成功,微信发送的callback信息,可以开始修改订单信息");
InputStream is = null;
try {
is = request.getInputStream();// 获取请求的流信息(这里是微信发的xml格式所以只能使用流来读)
String xml = WXPayUtil.InputStream2String(is);// 流转换为String
Map<String, String> notifyMap = WXPayUtil.xmlToMap(xml);// 将微信发的xml转map
//System.out.println("微信返回给回调函数的信息为:"+xml);
//支付成功进入
if (notifyMap.get("result_code").equals("SUCCESS")) {
// 告诉微信服务器收到信息了,不要在调用回调action了========这里很重要回复微信服务器信息用流发送一个xml即可
response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");
// 商户订单号
String ordersSn = notifyMap.get("out_trade_no");
// 实际支付的订单金额:单位 分
String amountpaid = notifyMap.get("total_fee");
// 将分转换成元-实际支付金额:元
BigDecimal amountPay = (new BigDecimal(amountpaid).divide(new BigDecimal("100"))).setScale(2);
//下面做业务处理
System.out.println("===notify===回调方法已经被调!!!");
//比如修改下单时间、订单状态等等...
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
再回调的callBack方法中,WXPayUtil.InputStream2String(is)方法可能会报错,原因可能是微信有更好的方法替代,还可以在WXPayUtil类中添加该方法的实现,具体如下:
/**
* fazcube
* @param is
* @return
* @throws IOException
*/
public static String InputStream2String(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
baos.close();
is.close();
byte[] lens = baos.toByteArray();
String result = new String(lens,"UTF-8");//内容乱码处理
return result;
}
至此,后端代码结束。
2. 微信退款开发
微信退款和微信支付差不多,也是凑齐参数发送给微信。
微信申请退款需要双向证书!(重要)
微信双向证书可以在微信商户频台-》账户设置-》API安全中下载,解压之后为以下文件:
JAVA只需要 apiclient_cert.p12 这个证书文件 。
双击apiclient.p12文件,安装一直下一步到输入私钥密码,密码为商户号(mch_id),一直下一步,直至提示导入成功,至此证书安装成功。
以下是后端代码:
/**
* 微信申请退款
* @param jsonObject
* @return
* @throws Exception
*/
@RequestMapping("/refund")
public Result<?> wxRefund(@RequestBody JSONObject jsonObject) throws Exception {
Map<String, String> params = new HashMap<>();
//订单总金额,这个根据自身业务来。
String orderPrice = "69900";
String refundPrice = "69900";
String outRefundNo = "你的退款号";//可以自定义一个类来随机生成退款单号等等
String refundMark = "退款备注";
params.put("appid", "appid");
params.put("mch_id", "mch_id");
//商户订单号
params.put("out_trade_no", "orderId");
//商户退款单号
params.put("out_refund_no", outRefundNo);
//总金额
params.put("total_fee", orderPrice);
//退款金额
params.put("refund_fee", refundPrice);
//退款原因
params.put("refund_desc", refundMark);
//退款结果回调地址
/*params.put("notify_url", "");*/
//随机字符串
params.put("nonce_str", WXPayUtil.generateNonceStr());//使用微信随机字符串生成
//生成sign
String sign = WXPayUtil.generateSignature(params, WxData.API_SECRET);
params.put("sign", sign);
//微信申请退款接口
String wx_refund_url = "https://api.mch.weixin.qq.com/secapi/pay/refund";
String xmlStr = WXPayUtil.mapToXml(params);//转换成xml格式
//发送双向证书请求给微信
String resultXmlStr = CertUtil.doRefund(wx_refund_url,xmlStr);
// 将返回的字符串转成Map集合
Map<String, String> resultMap = WXPayUtil.xmlToMap(resultXmlStr);//转成map格式
Map<String, String> map = new HashMap<>();
if ("SUCCESS".equalsIgnoreCase(resultMap.get("result_code"))) {
System.out.println("------申请退款成功,正在退款中----");
//申请微信退款接口返回success,但是退款到账还需要时间;
map.put("success", "REFUNDS");
} else {
map.put("success", resultMap.get("err_code_des"));
System.out.println("------退款失败----{}" + resultMap.get("err_code_des"));
}
return Result.OK(map);
}
微信退款的代码和微信支付的代码差不多,主要是退款需要微信双向证书绑定。
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.core.io.ClassPathResource;
public class CertUtil {
public static String doRefund(String url, String data) throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// 指定证书路径
String path = "/cert/apiclient_cert.p12";
ClassPathResource classPathResource = new ClassPathResource(path);
//读取本机存放的PKCS12证书文件
InputStream stream = classPathResource.getInputStream();
String mchId = "你的商户id";
try {
//指定PKCS12的密码(商户ID)
keyStore.load(stream, mchId.toCharArray());
} finally {
stream.close();
}
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, mchId.toCharArray()).build();
//指定TLS版本
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( sslcontext,new String[] { "TLSv1"},null,SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
//设置httpclient的SSLSocketFactory
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
try {
HttpPost httpost = new HttpPost(url); // 设置响应头信息
httpost.addHeader("Connection", "keep-alive");
httpost.addHeader("Accept", "*/*");
httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
httpost.addHeader("Host", "api.mch.weixin.qq.com");
httpost.addHeader("X-Requested-With", "XMLHttpRequest");
httpost.addHeader("Cache-Control", "max-age=0");
httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
httpost.setEntity(new StringEntity(data, "UTF-8"));
CloseableHttpResponse response = httpclient.execute(httpost);
try {
HttpEntity entity = response.getEntity();
String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
EntityUtils.consume(entity);
return jsonStr;
} finally {
response.close();
}
} finally {
httpclient.close();
}
}
}
在我的项目中,我将证书文件放置在模块的resources文件夹下面,具体项目结构如下:
用以上的方法可以直接读取。
注意:maven打包转码问题!!!
程序运行时报错:
java.io.IOException: DerInputStream.getLength(): lengthTag=111, too big.
原因:maven打包时,会对文件进行转码,重新编码后会导致证书文件不可用。
解决:pom依赖中进行配置,让maven打包时过滤掉不需要转码的文件:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>p12</nonFilteredFileExtension>
<nonFilteredFileExtension>pem</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
至此,微信退款接口开发完毕,如果项目有需求,还可以配置微信退款回调。