Java实现Native微信支付 超完整流程!!!
前言:最近公司准备在平板“***App”订单页面新建收款页完成收款功能,实现 微信扫码、支付宝扫码、网银扫码 收款功能。
- 项目环境介绍
- 微信支付文档
2.1 业务流程说明 - 前期准备
3.1 微信公众账号如何获取?
3.2 商户号如何获取?
3.3 API密钥如何获取? - 后端接口编写
4.1 依赖和工具类
4.2 生成二维码链接接口
4.3 查询订单支付状态接口
4.4 关闭交易订单接口
4.5 统一下单支付成功之后回调通知接口
总结
1.项目环境介绍
jdk 1.8
postgresql(远程)
maven 3.6.2
备注(该文章只展示后端实现流程)
2. 微信支付文档
官方文档:https://pay.weixin.qq.com/wiki/doc/api/index.html 选择Native支付
2.1 业务流程说明
1.根据公司系统订单,新建收款,触发后端接口“生成微信支付二维码”,后端返回二维码链接code_url给前端,前端转成二维码,用户通过微信扫码支付。
2.通过接口生成支付链接后,触发后端接口“查询订单支付状态”该接口在指定的一个时间内重复校验,在规定时间内状态都为未支付状态,则系统判定支付失效,给前端返回"二维码失效",触发后端“关闭订单接口”。
3.二维码展示给客户,客户支付完成之后完成业务逻辑,修改订单状态,生成订单信息等等。调“支付成功回调接口”
3. 前期准备
3.1 获取微信公众账号
公众账号ID是什么:微信支付分配的公众账号ID(公众号appid)
申请地址:https://mp.weixin.qq.com/ 该地址申请公众号,完成之后会分配公众号appId.公众号可关联
3.2 商户号如何获取?
商户号是什么:微信支付分配的商户号,关联公众号
申请地址:https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal
3.3 API密钥如何获取?
获取地址:https://mp.weixin.qq.com/ 该地址申请公众号,完成之后会分配AppSecret
4. 后端接口编写
4.1 依赖和工具类
在pom 文件引入微信支付 SDK 依赖
<!-- 微信支付 SDK -->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
自定义微信支付工具类
import org.apache.commons.lang.StringUtils;
/**
* 自定义微信支付工具类
*
* @author: fuqin
* @date: 2022/1/7
*/
public class CommonUtil {
/**
* 发送 http 请求
*
* @param requestUrl 请求路径
* @param requestMethod 请求方式(GET/POST/PUT/DELETE/...)
* @param outputStr 请求参数体
* @return 结果信息
*/
public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
try {
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
return buffer.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取ip
*
* @param request 请求
* @return ip 地址
*/
public static String getIp(HttpServletRequest request) {
if (request == null) {
return "";
}
String ip = request.getHeader("X-Requested-For");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
4.2 生成二维码链接接口
/**
* 生成微信支付二维码
*
* @param product_id 订单号//商品ID,系统内部订单id
* @param totalFee 金额(分)
*/
@RequestMapping(value = "/createNative",method = RequestMethod.GET)
public Map<String, String> createNative(@RequestParam("product_id") String product_id,
@RequestParam("totalFee") String totalFee,
HttpServletRequest request, HttpServletResponse response) {
try {
//todo 创建请求参数
SortedMap<String, String> req = new TreeMap<String, String>();
req.put("appid", "***"); //公众号appid
req.put("mch_id", "***"); // 商户号
req.put("nonce_str", WXPayUtil.generateNonceStr()); // 32位随机字符串
req.put("body", "***"); // 商品描述
String out_trade_no = Randoms.uuid();
req.put("out_trade_no", out_trade_no); // 商户订单号
req.put("product_id",product_id);//商品id,系统内部订单号
req.put("total_fee", df.format(Double.parseDouble(totalFee) * 100)); // 标价金额(元)
req.put("spbill_create_ip", CommonUtil.getIp(request)); // 终端IP
req.put("notify_url", "https://**/mtzx/WeiXinPay/pay/notify"); // 回调地址
req.put("trade_type", "NATIVE"); // 交易类型
req.put("sign", WXPayUtil.generateSignature(req, "APPSecret", WXPayConstants.SignType.MD5)); // 签名
// 生成要发送的 xml
String xmlBody = WXPayUtil.generateSignedXml(req, "APPSecret");
logger.info("微信支付预下单请求参数xmlBody={}", WXPayUtil.xmlToMap(xmlBody));
//发送 POST 请求 统一下单 API 并携带 xmlBody 内容,然后获得返回结果
String result = CommonUtil.httpsRequest("https://api.mch.weixin.qq.com/pay/unifiedorder", "POST", xmlBody);
logger.info("微信支付预下单请求结果result={}", WXPayUtil.xmlToMap(result));
//将返回结果从 xml 格式转换为 map 格式
Map<String, String> resultMap = WXPayUtil.xmlToMap(result);
Map<String, String> map = new HashMap<>();
String result_code = resultMap.get("result_code");
if (result_code.equals("FAIL")){
map.put("err_code_des",resultMap.get("err_code_des"));
map.put("status","201");
return map;
}
map.put("code_url", resultMap.get("code_url")); // 支付地址
map.put("total_fee", totalFee); // 总金额
map.put("out_trade_no", out_trade_no);// 随机生成商户订单号
map.put("status","200");
return map;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
备注:在pom文件引入了微信支付 SDK 就有WXPayUtil工具类,支付地址返回给前端,或者后端转二维码,此文章未写。
4.3 查询订单支付状态接口
/**
* 查询订单支付状态
* @author: fuqin
* @date: 2022/1/7
* @param outTradeNo 订单号
* @return 支付状态
*/
@RequestMapping(value = "/queryOrder", method = RequestMethod.GET)
public JsonResult queryOrder(@RequestParam("outTradeNo") String outTradeNo) {
int x = 0;
while (true) {
// 调用查询微信支付订单状态方法
Map<String, String> map = this.queryPayStatus(outTradeNo);
if (map.isEmpty()) {
return JsonResult.error("支付出错");
}
if (map.get("trade_state").equals("SUCCESS")) {
return JsonResult.ok("支付成功");
}
if (map.get("trade_state").equals("CLOSED")) {
return JsonResult.ok("订单已关闭");
}
try {
// 睡3秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 自定义时间,这期间未支付则提示二维码超时,前端在触发关闭订单接口
x++;
if (x >= 20) {
return JsonResult.error("二维码超时!");
}
}
}
Controller层中 queryPayStatus(String outTradeNo)方法如下:
/**
* 查询微信支付订单状态
* @author: fuqin
* @date: 2022/1/7
* @param outTradeNo 订单号
* @return 支付状态
*/
private Map<String, String> queryPayStatus(String outTradeNo) {
try {
//todo 创建请求参数
SortedMap<String, String> req = new TreeMap<String, String>();
req.put("appid", "**"); // 公众号ID
req.put("mch_id", "**"); // 商户号
req.put("out_trade_no", outTradeNo); // 订单号
req.put("nonce_str", WXPayUtil.generateNonceStr()); // 随机字符串
req.put("sign", WXPayUtil.generateSignature(req, "APPSecret", WXPayConstants.SignType.MD5));
//生成要发送的 xml
String xmlBody = WXPayUtil.generateSignedXml(req, "APPSecret");
logger.info("订单支付状态请求参数={}", WXPayUtil.xmlToMap(xmlBody));
//调用查询订单支付状态 API
String result = CommonUtil.httpsRequest("https://api.mch.weixin.qq.com/pay/orderquery", "POST", xmlBody);
// 返回解析后的 map 数据 转map
Map<String, String> map = WXPayUtil.xmlToMap(result);
logger.info("订单支付状态请求结果={}", WXPayUtil.xmlToMap(result));
return map;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
4.4 关闭交易订单接口
自定义时间,这期间未支付则提示二维码超时,前端在触发该接口
/**
* 关闭交易订单
* @author: fuqin
* @date: 2022/1/7
* @param outTradeNo
* @return 交易关闭状态
*/
@RequestMapping(value = "/closeOrder", method = RequestMethod.GET)
public Map<String, String> closeOrder(@RequestParam("outTradeNo") String outTradeNo) {
try {
SortedMap<String, String> req = new TreeMap<>();
req.put("appid", "***"); //公众号appid
req.put("mch_id", "***"); // 商户号
req.put("nonce_str", WXPayUtil.generateNonceStr()); // 32位随机字符串
req.put("out_trade_no", outTradeNo);//订单号
req.put("sign", WXPayUtil.generateSignature(req, "APPSecret", WXPayConstants.SignType.MD5)); // 签名
//生成发送请求的xml
String xmlBody = WXPayUtil.generateSignedXml(req, "APPSecret");
logger.info("关闭交易订单请求的xmlBody={}", WXPayUtil.xmlToMap(xmlBody));
String result = CommonUtil.httpsRequest("https://api.mch.weixin.qq.com/pay/closeorder", "POST", xmlBody);
logger.info("关闭交易订单请求结果={}", WXPayUtil.xmlToMap(result));
// 将返回结果从 xml 格式转换为 map 格式
Map<String, String> resultMap = WXPayUtil.xmlToMap(result);
Map<String, String> map = new HashMap<>();
String result_code = resultMap.get("result_code");
if (result_code.equals("FAIL")) {
map.put("err_code_des", resultMap.get("err_code_des"));
map.put("status", "201");
return map;
}
map.put("status", "200");
map.put("msg", "交易关闭成功");
return map;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
4.5 统一下单支付成功之后回调通知接口
客户支付成功,微信调我们接收回调接口,此接口可以做我们需要的业务逻辑,该地址在"4.2生成二维码链接接口"配置。注意地址必须是https协议且无参数
Controller层
/**
* 统一下单支付成功之后回调通知接口
* @author: fuqin
* @date: 2022/1/7
*/
@RequestMapping(value = "/pay/notify")
public void payAsyncNotify(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
logger.info("支付回调是否执行");
/*微信支付异步通知验证签名*/
String resultXml = wxPayService.payAsyncNotifyVerificationSign(httpServletRequest);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(httpServletResponse.getOutputStream());
bufferedOutputStream.write(resultXml.getBytes());
bufferedOutputStream.flush();
bufferedOutputStream.close();
}
WxPayService
public interface WxPayService {
/**
* 微信支付异步通知验证签名
* @param httpServletRequest
* @return
*/
String payAsyncNotifyVerificationSign(HttpServletRequest httpServletRequest);
}
WxPayServiceImpl
package net.dunotech.mercury.mtzx.service.WxPay.impl;
import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayUtil;
import net.dunotech.mercury.mtzx.service.WxPay.WxPayService;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;
/**
* 微信支付成功回调通知
*
* @author: fuqin
* @date: 2022/1/7
*/
@Service
public class WxPayServiceImpl implements WxPayService {
private static Logger logger = LoggerFactory.getLogger(WxPayServiceImpl.class);
@Override
public String payAsyncNotifyVerificationSign(HttpServletRequest httpServletRequest) {
/* WXPay wxPay = new WXPay(myWxPayConfig);*/
String returnXmlMessage = null;
String notifyXmlData = null;
try {
notifyXmlData = readXmlFromStream(httpServletRequest);
Map<String, String> notifyMapData = WXPayUtil.xmlToMap(notifyXmlData);
logger.info("微信支付成功回调notifyMapData={}", notifyMapData);
// 验证签名
/* boolean signatureValid = wxPay.isPayResultNotifySignatureValid(notifyMapData);*/
// 验证是否支付成功
if("SUCCESS".equals(notifyMapData.get( "result_code"))){
// 获取支付成功的订单号
String out_trade_no = notifyMapData.get("out_trade_no");
//订单支付成功之后相关业务逻辑...
logger.info("支付成功回调订单号out_trade_no={}",out_trade_no);
// 一切正常返回的xml数据
returnXmlMessage = setReturnXml(WXPayConstants.SUCCESS, "OK");
logger.info("[out_trade_no:{}] [支付成功异步消息处理成功:{}]", notifyMapData.get("out_trade_no"), WXPayUtil.xmlToMap(returnXmlMessage));
} else {
returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "Verification sign failed!");
logger.info("[out_trade_no:{}] [验签失败:{}]", notifyMapData.get("out_trade_no"), WXPayUtil.xmlToMap(returnXmlMessage));
}
} catch (IOException e) {
logger.error("[读取微信服务器返回流中xml数据时发生异常:{}] ", ExceptionUtils.getStackTrace(e));
returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "An exception occurred while reading the WeChat server returning xml data in the stream.");
} catch (Exception e) {
logger.error("[xml数据:{}] [异常:{}] ", notifyXmlData, ExceptionUtils.getStackTrace(e));
returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "Payment successful, exception occurred during asynchronous notification processing.");
logger.warn("[支付成功异步消息处理失败:{}]", returnXmlMessage);
}
return returnXmlMessage;
}
/**
* 从流中读取微信返回的xml数据
*
* @param httpServletRequest
* @return
* @throws IOException
*/
private String readXmlFromStream(HttpServletRequest httpServletRequest) throws IOException {
InputStream inputStream = httpServletRequest.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
final StringBuffer sb = new StringBuffer();
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
} finally {
bufferedReader.close();
inputStream.close();
}
return sb.toString();
}
/**
* 设置返回给微信服务器的xml信息
*
* @param returnCode
* @param returnMsg
* @return
*/
private String setReturnXml(String returnCode, String returnMsg) {
return "<xml><return_code><![CDATA[" + returnCode + "]]></return_code><return_msg><![CDATA[" + returnMsg + "]]></return_msg></xml>";
}
}
总结
写的不好,欢迎大佬们点评。后期有时间还也会更新 支付宝 及 网银支付全流程。