摘要
最近因为项目需要微信扫码支付功能,在网上找了很久的微信扫码支付模式二的案例,发现很多要么都是代码不全,要么就是代码错误。经过查找许多资料以及编写测试后,终于成功解决微信扫码支付模式二。
模式二与模式一相比,流程更为简单,不依赖设置的回调支付URL。商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。注意:code_url有效期为2小时,过期后扫码不能再发起支付。
微信扫码支付流程时序图
业务流程说明:
(1)商户后台系统根据用户选购的商品生成订单。
(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
(4)商户后台系统根据返回的code_url生成二维码。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
(8)微信支付系统根据用户授权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
(11)未收到支付通知的情况,商户后台系统调用【查询订单API】。
(12)商户确认订单已支付后给用户发货。
Java代码案例
微信扫码支付操作流程:
- 后台设置公众账号ID appid、商户号 mch_id、随机字符串 nonce_str、签名 sign、商品描述(可让前台传入) body、附加数据(可略) attach、商户订单号(可随机生成/前台传入) out_trade_no、服务器ip地址 spbill_create_ip、商户号密钥 key、微信回调接口 notify_url、扫码支付交易类型 trade_type 。
- 前台提交订单传支付金额到后台,后台生成微信支付二维码链接(codeUrl)和商户订单号(out_trade_no)给前台。
- 设置微信回调方法和回调接口,让微信自动回调。
- 前台页面设置定时调用后台查询订单支付状态链接,查询订单状态(订单未支付/支付成功)。
微信商户平台的扫码支付回调链接设置:
这里使用的是微信Nature扫码支付模式二,不需要安装操作证书。设置微信商户平台的扫码支付回调链接,让微信自己调用回调方法。微信商户平台的扫码支付回调链接设置如下图:
下面是微信扫码支付模块代码图:
下方代码只需更改【微信公众账号appid、微信商户号mch_id、服务器ip地址spbill_create_ip、微信回调接口notify_url、商户号密钥key】,其他信息可根据实际信息更改。
案例微信支付接口:http://www.xxx.cn/wxpay/nativePay
案例微信回调接口:http://www.xxx.cn/wxpay/results
案例微信查询微信订单状态接口:http://www.xxx.cn/wxpay/query
WeiXinPayController类
package com.aaa.project.tool.WeChatPay.controller;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.XmlUtil;
import com.aaa.project.tool.CORS.Cors;
import com.aaa.project.tool.WeChatPay.utils.WeiXinUtil;
import com.aaa.project.tool.WeChatPay.utils.XMLUtil4jdom;
import org.jdom2.JDOMException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
@Controller
@RequestMapping("/wxpay")
public class WeiXinPayController {
private String appid="xxxxxxxxxxxxx";//此处填写微信公众账号ID
private String mch_id="xxxxxxxxx";//此处填写微信商户号
private String spbill_create_ip="xxx.xxx.xxx.xxx";//此处填写服务器ip地址
private String notify_url= "http://www.xxx.cn/wxpay/results";//此处填写微信回调接口
private String key="xxxxxxxxxxxxxx";//此处填写商户号密钥key
private static final String UNIFIEDORDERURL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
private static final String ORDERQUERYURL = "https://api.mch.weixin.qq.com/pay/orderquery";
@Autowired
private RestTemplate restTemplate;
/**
* 微信支付
* 支付的金额 paymoney
* @return
*/
@RequestMapping("/nativePay")
@ResponseBody
public Map<String, Object> nativePay( String paymoney,HttpServletResponse response, HttpServletRequest request){
//随机字符串
String nonce_str=RandomUtil.randomString(15);
//商户订单号(随机生成)
String out_trade_no=new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())+RandomUtil.randomInt(6);
/*配置微信支付基础信息参数*/
Map<String, String> requestData = new HashMap<String, String>();
requestData.put("appid", appid);//公众账号ID
requestData.put("mch_id",mch_id );//商户号
requestData.put("nonce_str",nonce_str);//随机字符串 32位以内
// APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。
requestData.put("spbill_create_ip", spbill_create_ip);
requestData.put("trade_type", "NATIVE");//交易类型 扫码支付
/*配置微信支付自定义支付信息参数*/
requestData.put("attach", "附加数据返回");
requestData.put("body", "商品描述");//商品简单描述
requestData.put("out_trade_no", out_trade_no);//商户订单号
requestData.put("total_fee", WeiXinUtil.getMoney(paymoney));//标价金额 按照分(0.01)进行计算
requestData.put("notify_url",notify_url);//通知地址 异步接收微信支付结果通知的回调地址必须外网访问 不能携带参数
/*配置微信支付sign信息参数*/
String sign = WeiXinUtil.generateSign(requestData,key);
String payUrl = UNIFIEDORDERURL;
requestData.put("sign", sign);
/*将map信息转换成String*/
String mapToXmlStr = XmlUtil.mapToXmlStr(requestData, "xml");
/*调用微信统一下单Api将mapToXmlStr作为参数*/
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
HttpEntity<String> formEntity = new HttpEntity<>(mapToXmlStr, headers);
ResponseEntity<String> postForEntity = restTemplate.postForEntity(payUrl, formEntity, String.class);
//获取微信返回的信息
String returnXmlString = postForEntity.getBody();
Map<String, Object> xmlToMap = XmlUtil.xmlToMap(returnXmlString);
String returnCode = (String)xmlToMap.get("return_code");
Map<String, Object> map=new HashMap<>();
map.put("out_trade_no",out_trade_no);
if("SUCCESS".equals(returnCode)){
String codeUrl = (String)xmlToMap.get("code_url");
map.put("codeUrl",codeUrl);
//返回数据(商户订单号out_trade_no、二维码链接codeUrl)
return map;
}
return null;
}
/**
* 微信平台发起的回调方法
* 调用我们这个系统的这个方法接口,将扫描支付的处理结果告知我们系统
* @throws JDOMException
* @throws Exception
*/
@RequestMapping("/results")
public void weixinNotify(HttpServletRequest request, HttpServletResponse response) throws JDOMException, Exception{
//读取参数
InputStream inputStream ;
StringBuffer sb = new StringBuffer();
inputStream = request.getInputStream();
String s ;
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
while ((s = in.readLine()) != null){
sb.append(s);
}
in.close();
inputStream.close();
//解析xml成map
Map<String, String> m = new HashMap<String, String>();
m = XMLUtil4jdom.doXMLParse(sb.toString());
//过滤空 设置 TreeMap
SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
Iterator it = m.keySet().iterator();
while (it.hasNext()) {
String parameter = (String) it.next();
String parameterValue = m.get(parameter);
String v = "";
if(null != parameterValue) {
v = parameterValue.trim();
}
packageParams.put(parameter, v);
}
Map<String, String> parseNotifyParameter = WeiXinUtil.parseNotifyParameter(request);
String sign = WeiXinUtil.generateSign(parseNotifyParameter,key);//生成签名
//判断签名是否正确
if(sign.equals(parseNotifyParameter.get("sign"))){
//处理业务开始
String resXml = "";
if("SUCCESS".equals((String)packageParams.get("result_code"))&&(String)packageParams.get("result_code")!=null){
// 这里是支付成功
//执行自己的业务逻辑,如果有数据库表,可以添加到数据库中
// String mch_id = (String)packageParams.get("mch_id");
// String openid = (String)packageParams.get("openid");
// String is_subscribe = (String)packageParams.get("is_subscribe");
// String out_trade_no = (String)packageParams.get("out_trade_no");
// String total_fee = (String)packageParams.get("total_fee");
//暂时使用最简单的业务逻辑来处理:只是将业务处理结果保存到session中
//(根据自己的实际业务逻辑来调整,很多时候,我们会操作业务表,将返回成功的状态保留下来)
request.getSession().setAttribute("_PAY_RESULT", "OK");
System.out.println("支付成功");
//通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
} else {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
}
//处理业务完毕
BufferedOutputStream out = new BufferedOutputStream(
response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} else{
System.out.println("通知签名验证失败");
}
}
/**
* 前台定时查询支付结果,由前台设置定时任务,查询订单状态信息
* 商户订单号 out_trade_no
* @return
*/
@RequestMapping("/query")
@ResponseBody
public String query(String out_trade_no,HttpServletResponse response, HttpServletRequest request){
//随机字符串
String nonce_str=RandomUtil.randomString(15);
if(StringUtils.isBlank(out_trade_no)){
throw new RuntimeException("订单号不能为空!");
}
//配置微信支付基础信息参数
Map<String, String> requestData = new HashMap<String, String>();
requestData.put("appid", appid);//公众账号ID
requestData.put("mch_id", mch_id);//商户号
requestData.put("nonce_str", nonce_str);//随机字符串
requestData.put("spbill_create_ip", spbill_create_ip);
//配置微信支付查询订单号参数
requestData.put("out_trade_no", out_trade_no);//商户订单号
//配置微信支付查询sign信息参数
String sign = WeiXinUtil.generateSign(requestData,key);//生成签名
requestData.put("sign", sign);
//将map信息转换成String
String mapToXmlStr = XmlUtil.mapToXmlStr(requestData, "xml");
//调用微信统一下单Api 将xml的String信息作为参数
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
HttpEntity<String> formEntity = new HttpEntity<>(mapToXmlStr, headers);
ResponseEntity<String> postForEntity = restTemplate.postForEntity(ORDERQUERYURL, formEntity, String.class);
//获取微信返回的信息
String returnXmlString = postForEntity.getBody();
Map<String, Object> xmlToMap = XmlUtil.xmlToMap(returnXmlString);
//获取微信返回的订单状态
String trade_state_desc = (String)xmlToMap.get("trade_state_desc");
String code="0";
if("订单未支付".equals(trade_state_desc)){
return code;
}else if("支付成功".equals(trade_state_desc)){
code="1";
return code;
}
return code;
}
}
RestTemplateConfig类
package com.aaa.project.tool.WeChatPay.resttemplate.config;
import com.aaa.project.tool.WeChatPay.resttemplate.support.CustomConnectionKeepAliveStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(100))
.setReadTimeout(Duration.ofMillis(500))
.requestFactory(this::requestFactory)
.build();
}
@Bean
public HttpComponentsClientHttpRequestFactory requestFactory() {
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(20);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.evictIdleConnections(30, TimeUnit.SECONDS)
.disableAutomaticRetries()
// 有 Keep-Alive 认里面的值,没有的话永久有效
//.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
// 换成自定义的
.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
return requestFactory;
}
}
WXUser类(此类可有可无)
package com.aaa.project.tool.WeChatPay.resttemplate.model;
import java.io.Serializable;
public class WXUser implements Serializable{
private String name;
private Integer age;
private String addr;
public String getAddr() {
return addr;
}
public void setAddr(String addr) {
this.addr = addr;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
CustomConnectionKeepAliveStrategy类
package com.aaa.project.tool.WeChatPay.resttemplate.support;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.HttpResponse;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import java.util.Arrays;
public class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
private final long DEFAULT_SECONDS = 30;
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
return Arrays.asList(response.getHeaders(HTTP.CONN_KEEP_ALIVE))
.stream()
.filter(h -> StringUtils.equalsIgnoreCase(h.getName(), "timeout")
&& StringUtils.isNumeric(h.getValue()))
.findFirst()
.map(h -> NumberUtils.toLong(h.getValue(), DEFAULT_SECONDS))
.orElse(DEFAULT_SECONDS) * 1000;
}
}
WeiXinUtil类
package com.aaa.project.tool.WeChatPay.utils;
import cn.hutool.crypto.SecureUtil;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.*;
import java.util.Map.Entry;
public class WeiXinUtil {
/**
* 创建md5摘要,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
*/
public static String generateSign(Map<String, String> requestData,String key) {
TreeMap<String, String> sortMapByKey = (TreeMap<String, String>) sortMapByKey(requestData);
StringBuffer keyWithValue = splicingKeyAndValue(sortMapByKey);
keyWithValue.append("key=" + key);
String sign = SecureUtil.md5().digestHex(keyWithValue.toString(), "UTF-8");
return sign;
}
/**
* 拼接Map中的key和value通过&
* @param sortMapByKey
* @return
*/
private static StringBuffer splicingKeyAndValue(TreeMap<String, String> sortMapByKey) {
StringBuffer sb = new StringBuffer();
Set<Entry<String, String>> es = sortMapByKey.entrySet();
Iterator<Entry<String, String>> it = es.iterator();
while (it.hasNext()) {
Entry<String, String> entry = (Entry<String, String>) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k)
&& !"key".equals(k) && ! "sign_type".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
return sb;
}
/**
* Map 按key排序
* @param oriMap
* @return
*/
public static Map<String, String> sortMapByKey(Map<String, String> oriMap) {
if (oriMap == null || oriMap.isEmpty()) {
return null;
}
Map<String, String> sortedMap = new TreeMap<String, String>();
sortedMap.putAll(oriMap);
return sortedMap;
}
/**
* 元转换成分
* @param //money
* @return
*/
public static String getMoney(String amount) {
if(amount==null){
return "";
}
// 金额转化为分为单位
String currency = amount.replaceAll("\\$|\\¥|\\,", ""); //处理包含, ¥ 或者$的金额
int index = currency.indexOf(".");
int length = currency.length();
Long amLong = 0l;
if(index == -1){
amLong = Long.valueOf(currency+"00");
}else if(length - index >= 3){
amLong = Long.valueOf((currency.substring(0, index+3)).replace(".", ""));
}else if(length - index == 2){
amLong = Long.valueOf((currency.substring(0, index+2)).replace(".", "")+0);
}else{
amLong = Long.valueOf((currency.substring(0, index+1)).replace(".", "")+"00");
}
return amLong.toString();
}
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder= documentBuilderFactory.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) {
return data;
}
/**
* 从request的inputStream中获取参数
* @param request
* @return
* @throws Exception
*/
public static Map<String, String> parseNotifyParameter(HttpServletRequest request) throws Exception {
InputStream inputStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = 0;
while ((length = inputStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, length);
}
outSteam.close();
inputStream.close();
// 获取微信调用我们notify_url的返回信息
String resultXml = new String(outSteam.toByteArray(), "utf-8");
Map<String, String> notifyMap = WeiXinUtil.xmlToMap(resultXml);
return notifyMap;
}
}
XMLUtil4jdom类
package com.aaa.project.tool.WeChatPay.utils;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class XMLUtil4jdom {
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map doXMLParse(String strxml) throws JDOMException, IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if(null == strxml || "".equals(strxml)) {
return null;
}
Map<String, String> m = new HashMap<String, String>();
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if(children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = XMLUtil4jdom.getChildrenText(children);
}
m.put(k, v);
}
//关闭流
in.close();
return m;
}
/**
* 获取子结点的xml
* @param children
* @return String
*/
public static String getChildrenText(List children) {
StringBuffer sb = new StringBuffer();
if(!children.isEmpty()) {
Iterator it = children.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List list = e.getChildren();
sb.append("<" + name + ">");
if(!list.isEmpty()) {
sb.append(XMLUtil4jdom.getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
}
最后
上面就是微信扫码支付模式二的Java代码案例,如有疑问都可以在下面评论留言,作者会帮忙解答。