目录
开发前准备
1、登录微信公众后台:小程序开通微信支付并绑定商户号(在小程序后台微信支付模块按提示操作即可)
2、登录商户平台:商户号对小程序绑定授权(产品中心-APPID授权管理,另外,如果是公众号还需在开发配置中配置支付授权目录)
注意:如果以上2条没有配置好则后面接口返回会出现 appid 和 mch_id 不匹配 (Lz第一次入坑)
3、准备好小程序的appid,微信支付的商户号,支付秘钥
4、小程序内调用登录接口,获取到用户的openid(小程序登录开发指南)
小程序支付开发帮助文档
JSAPI支付
小程序支付业务流程图
统一下单API接口帮助文档
接下来给大家分享实现代码
微信支付配置文件:Configure
注意 : Configure 中 key不是商户秘钥,而是手动生成的key,LZ第二次入坑,key写的是商户密钥,出现签名错误,为此专门记录一篇博客,希望大家别再掉坑里了,看key如何生成戳这里 > 微信小程序支付统一下单接口返回签名错误已解决
/**
* @ClassName: Configure
* @Author: liuyaolong
* @Description: 微信端支付配置信息
* @Version: 1.0
*/
public class Configure {
//微信小程序appid
public static String appId = "";
//微信小程序appsecret
public static String appSecret = "";
//微信商户号
public static String mch_id="";
//手动生成的key
public static final String key = "";
//获取微信Openid的请求地址
public static String WxGetOpenIdUrl = "";
//支付成功后的服务器回调url
public static final String notify_url="";
//签名方式
public static final String SIGNTYPE = "MD5";
//交易类型
public static final String TRADETYPE = "JSAPI";
//微信统一下单接口地址
public static final String pay_url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
public static String getAppId() {
return appId;
}
public static void setAppId(String appId) {
Configure.appId = appId;
}
public static String getAppSecret() {
return appSecret;
}
public static void setAppSecret(String appSecret) {
Configure.appSecret = appSecret;
}
public static String getMch_id() {
return mch_id;
}
public static void setMch_id(String mch_id) {
Configure.mch_id = mch_id;
}
public static String getKey() {
return key;
}
public static String getWxGetOpenIdUrl() {
return WxGetOpenIdUrl;
}
public static void setWxGetOpenIdUrl(String wxGetOpenIdUrl) {
WxGetOpenIdUrl = wxGetOpenIdUrl;
}
public static String getNotify_url() {
return notify_url;
}
public static String getSIGNTYPE() {
return SIGNTYPE;
}
public static String getTRADETYPE() {
return TRADETYPE;
}
public static String getPay_url() {
return pay_url;
}
}
PayUtils在使用时需要在Maven中配置SAXBuilder和Gson
SAXBuilder解读分析: Java解析XML之SAXBuilder用法
maven配置SAXBuilder和Gson
<properties>
<gson.version>2.2.4</gson.version>
<saxbuilder.version>2.0.5</saxbuilder.version>
</properties>
<!-- SAXBuilder -->
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom2</artifactId>
<version>${saxbuilder.version}</version>
</dependency>
<!-- gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
微信支付工具类:PayUtils
/**
* @ClassName: PayUtils
* @Author: liuyaolong
* @Description: 微信支付工具类
* @Version: 1.0
*/
public class PayUtils {
/**
* Map值过滤,除去 Map中值为空值和签名参数
* @param params 签名参数
* @return
*/
public static Map<String,String> paramsFilter(Map<String,String> params){
Map<String,String> rsMap = new HashMap<String,String>();
if(params == null || params.size() <= 0){
return rsMap;
}
for (String key : params.keySet()){
String value = params.get(key);
if(value == null || value.equals("") || key.equalsIgnoreCase("sign") || key.equalsIgnoreCase("sign_type")){
continue;
}
rsMap.put(key,value);
}
return rsMap;
}
/**
* 把数组所有元素排序,并按照 “参数=参数值” 的模式用 “&” 字符拼接成字符串
* @param params 需要排序并参与字符拼接的参数组
* @return 拼接后字符串
*/
public static String createLinkString(Map<String, String> params) {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
String prestr = "";
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
prestr = prestr + key + "=" + value;
} else {
prestr = prestr + key + "=" + value + "&";
}
}
return prestr;
}
/**
* 签名字符串
* @param text 需要签名的字符串
* @param key 密钥
* @param input_charset 编码格式
* @return 签名结果
*/
public static String sign(String text, String key, String input_charset) {
text = text + "&key=" + key;
return DigestUtils.md5Hex(getContentBytes(text, input_charset));
}
/**
* 签名字符串
* @param text 需要签名的字符串
* @param sign 签名结果
* @param key 密钥
* @param input_charset 编码格式
* @return 签名结果
*/
public static boolean verify(String text, String sign, String key, String input_charset) {
text = text +"&key="+ key;
String mysign = DigestUtils.md5Hex(getContentBytes(text, input_charset));
if (mysign.equals(sign)) {
return true;
} else {
return false;
}
}
/**
* @param content
* @param charset
* @return
* @throws java.security.SignatureException
* @throws UnsupportedEncodingException
*/
public static byte[] getContentBytes(String content, String charset) {
if (charset == null || "".equals(charset)) {
return content.getBytes();
}
try {
return content.getBytes(charset);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset);
}
}
/** HttpRequest 请求
* @param requestUrl 请求地址
* @param requestMethod 请求方法
* @param outputStr 参数
*/
public static String httpRequest(String requestUrl, String requestMethod, String outputStr) {
// 创建SSLContext
StringBuffer buffer = null;
try {
URL url = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(requestMethod);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.connect();
//往服务器端写内容
if (null != outputStr) {
OutputStream os = conn.getOutputStream();
os.write(outputStr.getBytes("utf-8"));
os.close();
}
// 读取服务器端返回的内容
InputStream is = conn.getInputStream();
InputStreamReader isr = new InputStreamReader(is, "utf-8");
BufferedReader br = new BufferedReader(isr);
buffer = new StringBuffer();
String line = null;
while ((line = br.readLine()) != null) {
buffer.append(line);
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}
return buffer.toString();
}
public static String urlEncodeUTF8(String source) {
String result = source;
try {
result = java.net.URLEncoder.encode(source, "UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws org.jdom2.JDOMException
* @throws IOException
*/
public static Map doXMLParse(String strxml) throws Exception {
if (null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
InputStream in = StringInputstream(strxml);
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 = 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(getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
public static InputStream StringInputstream(String str) {
return new ByteArrayInputStream(str.getBytes());
}
/**
* java对象转字符串
* @param <T> 泛型占位符
* @param obj T
* @return String
*/
public static <T> String toJson(T obj) {
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").enableComplexMapKeySerialization().create();
return gson.toJson(obj);
}
获取客户端IP地址工具类:IPAddr
/**
* @ClassName: IPAddr
* @Author: liuyaolong
* @Description: IP地址工具类
* @Version: 1.0
*/
public class IPAddr {
/**
* 获取Ip地址,多级反向代理
* @param request
* @return
*/
public static String getIpaddr(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.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")){
//根据网卡取本机配置的IP
InetAddress inet=null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress= inet.getHostAddress();
}
}
//对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if(ipAddress!=null && ipAddress.length()>15){ //"***.***.***.***".length() = 15
if(ipAddress.indexOf(",")>0){
ipAddress = ipAddress.substring(0,ipAddress.indexOf(","));
}
}
return ipAddress;
}
}
请求支付订单基础参数
/**
* @ClassName: orderInfo
* @Author: liuyaolong
* @Description: 订单基础信息
* @Version: 1.0
*/
@Data
public class OrderInfo implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank(message = "orderId can not be null !")
private String orderId;//订单编号
@NotBlank(message = "title can not be null !")
private String title;//商品标题
@NotNull(message = "money can not be null !")
private Integer money;//订单金额,单位:分
@NotBlank(message = "openId can not be null !")
private String openId;//小程序用户唯一标识
private String intro;//商品介绍
}
调用微信支付接口Service
/**
* @ClassName: WeChatPayService
* @Author: liuyaolong
* @Description: 微信支付服务类
* @Version: 1.0
*/
@Service
public class WeChatPayService {
private static Logger logger = LoggerFactory.getLogger(WeChatPayService.class);
/**
* 微信支付
* @param ipAddr 用户IP地址
* @param orderInfo 订单信息
* @return
*/
public Map WeChatPay(String ipAddr, OrderInfo orderInfo){
Map<String,Object> map = new HashMap<String, Object>();
//生成的随机字符串32
String nonce_str = RandomStringGenerator.getRandomStringByLength(32);
try{
String body = new String(orderInfo.getTitle().getBytes("ISO-8859-1"),"UTF-8");
//参数组装,生成统一下单接口签名
Map<String,String> paramsMap = new HashMap<String,String>();
paramsMap.put("appid",Configure.getAppId());
paramsMap.put("mch_id",Configure.getMch_id());
paramsMap.put("nonce_str",nonce_str);
paramsMap.put("body",body);
paramsMap.put("out_trade_no",orderInfo.getOrderId());
paramsMap.put("total_fee",String.valueOf(orderInfo.getMoney()));
paramsMap.put("spbill_create_ip",ipAddr);
paramsMap.put("notify_url",Configure.getNotify_url());
paramsMap.put("trade_type",Configure.getTRADETYPE());
paramsMap.put("openid",orderInfo.getOpenId());
//参数过滤
paramsMap = PayUtils.paramsFilter(paramsMap);
//把所有数据拼接成字符串
String linkParams = PayUtils.createLinkString(paramsMap);
//MD5生成签名,用于调用统一下单接口
String sign = PayUtils.sign(linkParams,Configure.getKey(),"utf-8").toUpperCase();
//拼接统一下单接口所需xml数据
String xml = "<xml>" + "<appid>" + Configure.getAppId() + "</appid>"
+ "<body><![CDATA[" + body + "]]></body>"
+ "<mch_id>" + Configure.mch_id + "</mch_id>"
+ "<nonce_str>" + nonce_str + "</nonce_str>"
+ "<notify_url>" + Configure.notify_url + "</notify_url>"
+ "<openid>" + orderInfo.getOpenId() + "</openid>"
+ "<out_trade_no>" + orderInfo.getOrderId() + "</out_trade_no>"
+ "<spbill_create_ip>" + ipAddr + "</spbill_create_ip>"
+ "<total_fee>" + String.valueOf(orderInfo.getMoney()) + "</total_fee>"//支付的金额,单位:分
+ "<trade_type>" + Configure.TRADETYPE + "</trade_type>"
+ "<sign>" + sign + "</sign>"
+ "</xml>";
//调用统一下单接口,返回响应数据
String result = PayUtils.httpRequest(Configure.pay_url,"POST",xml);
//将解析结果存入Map中
Map rsMap = PayUtils.doXMLParse(result);
//返回状态码
String return_code = (String) rsMap.get("return_code");
String result_msg = (String) rsMap.get("result_msg");
String err_code = (String) rsMap.get("err_code");
String err_code_des = (String) rsMap.get("err_code_des");
if (return_code.equals("SUCCESS") || return_code.equals(return_code) ) {
//返回的预付单信息
String prepay_id = (String) rsMap .get("prepay_id");
map.put("nonceStr", nonce_str);
map.put("package", "prepay_id=" + prepay_id);
Long timeStamp = System.currentTimeMillis() / 1000;
//这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误
map.put("timeStamp", timeStamp + "");
//拼接签名需要的参数
String stringSignTemp = "appId=" + Configure.getAppId() + "&nonceStr=" + nonce_str + "&package=prepay_id=" + prepay_id + "&signType=MD5&timeStamp=" + timeStamp;
//再次签名,这个签名用于小程序端调用wx.requesetPayment方法
String paySign = PayUtils.sign(stringSignTemp, Configure.key, "utf-8").toUpperCase();
map.put("paySign", paySign);
} else {
logger.info("统一下单失败");
return map;
}
map.put("appid", Configure.getAppId());
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
后端统一下单和支付回调接口
/**
* @ClassName: WechatPayController
* @Author: liuyaolong
* @Description: 微信支付api控制层
* @Version: 1.0
*/
@RestController
@RequestMapping("/wechat")
public class WechatPayController {
@Resource
private WeChatPayService weChatPayService;
private static Logger logger = LoggerFactory.getLogger(WechatPayController.class);
/**
* 微信支付
* @param in
* @param request
* @param result
* @return
*/
@PostMapping("/pay")
public ResponseMsg<String> pay(@Valid @RequestBody RequestMsg<OrderInfo> in, HttpServletRequest request, BindingResult result) {
ResponseMsg<String> response = new ResponseMsg<>();
Object object;
try{
//获取客户端ip地址
String ip = IPAddr.getIpaddr(request);
object = weChatPayService.WeChatPay(ip,in.getData());
response.setData(PayUtils.toJson(object));
} catch (Exception e) {
e.printStackTrace();
}
logger.info("调用微信支付接口,入参信息为:{}",in.getData());
return response;
}
/**
* 支付回调接口
* @param request
* @param response
* @throws Exception
*/
@PostMapping("/notify")
public void notify(HttpServletRequest request, HttpServletResponse response) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream()));
StringBuilder sb = new StringBuilder();
BufferedOutputStream out = null;
String line = null;
try{
while((line = br.readLine()) != null){
sb.append(line);
}
//响应xml
String notifyXml = sb.toString();
String resXml = "";
Map map = PayUtils.doXMLParse(notifyXml);
String returnCode = (String) map.get("return_code");
if ("SUCCESS".equals(returnCode)) {
//验证签名是否正确,回调验签时需要去除sign和空值参数
Map<String, String> validParams = PayUtils.paramsFilter(map);
//把所有数据拼接成字符串
String prestr = PayUtils.createLinkString(validParams);
//根据微信官网的介绍,此处不仅对回调的参数进行验签,还需要对返回的金额与系统订单的金额进行比对等
if (PayUtils.verify(prestr, (String) map.get("sign"), Configure.getKey(), "utf-8")) {
/**此处添加自己的业务逻辑代码start**/
//注意要判断微信支付重复回调,支付成功后微信会重复的进行回调
/**此处添加自己的业务逻辑代码end**/
//通知微信服务器已经支付成功
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> ";
}
out = new BufferedOutputStream(response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
logger.info("微信支付接口回调 ! ");
} catch (Exception e) {
e.printStackTrace();
} finally {
br.close();
out.close();
}
}
}
返回参数到前端起调微信支付
以上就是本文的全部内容,希望对大家的学习有所帮助。