springboot对接微信支付的完整流程(附前后端代码

网友投稿 1358 2022-12-22

springboot对接微信支付的完整流程(附前后端代码)

springboot对接微信支付的完整流程(附前后端代码)

展示图:

对接的完整流程如下

首先是配置

wxPay.mchId=商户号

wxPay.key=支付密钥

wxPay.notifyUrl=域名回调地址

常量:

/**微信支付统一下单接口*/

public static final String unifiedOrderUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";

public static String SUCCESSxml = " \r\n" +

"\r\n" +

" <![CDATA[SUCCESS]]>\r\n" +

" <![CDATA[OK]]>\r\n" +

" \r\n" +

"";

public static String ERRORxml = " \r\n" +

"\r\n" +

" <![CDATA[FAIL]]>\r\n" +

" <![CDATA[invalid sign]]>\r\n" +

" \r\n" +

"";

工具类准备:

package com.jc.utils.util;

import org.apache.commons.codec.digest.DigestUtils;

import org.apache.commons.lang.StringUtils;

import org.apache.http.HttpEntity;

import org.apache.http.client.ClientProtocolException;

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.entity.StringEntity;

import org.apache.http.impl.client.CloseableHttpClient;

import org.apache.http.impl.client.HttpClients;

import org.apache.http.ssl.SSLContexts;

import org.apache.http.util.EntityUtils;

import org.jdom2.Document;

import org.jdom2.Element;

import org.jdom2.input.SAXBuilder;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import javax-.ssl.SSLContext;

import javax.servlet.http.HttpServletRequest;

import java.io.*;

import java-.HttpURLConnection;

import java-.URL;

import java.security.KeyStore;

import java.security.KeyStoreException;

import java.text.SimpleDateFormat;

import java.util.*;

public class CommUtils {

private static Logger logger = LoggerFactory.getLogger(CommUtils.class);

// 连接超时时间,默认10秒

private static int socketTimeout = 60000;

// 传输超时时间,默认30秒

private static int connectTimeout= 60000;

/**

* Util工具类方法

* 获取一定长度的随机字符串,范围0-9,a-z

* @param length:指定字符串长度

* @return 一定长度的随机字符串

*/

public static String getRandomStringByLength(int length) {

String base = "abcdefghijklmnopqrstuvwxyz0123456789";

Random random = new Random();

StringBuffer sb = new StringBuffer();

for (int i = 0; i < length; i++) {

int number = random.nextInt(base.length());

sb.append(base.charAt(number));

}

return sb.toString();

}

/**

* 获取订单号

* @return

*/

public static String getOrderNo(){

SimpleDateFormat ft = new SimpleDateFormat("yyyyMMddHHmmss");

String time = ft.format(new Date());

int mathCode = (int) ((Math.random() * 9 + 1) * 10000);// 5位随机数

String resultCode = time+mathCode;

return resultCode;

}

/**

* Util工具类方法

* 获取真实的ip地址

* @param request

* @return

*/

public static String getIpAddr(HttpServletRequest request) {

String ip = request.getHeader("X-Forwarded-For");

if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(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();

}

/**

* 签名字符串

* @param text 需要签名的字符串

* @param key 密钥

* @param input_charset 编码格式

* @return 签名结果

*/

public static String sign(String text, String key, String input_charset) {

text = text + "&key=" + key;

System.out.println(text);

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;

String mysign = DigestUtils.md5Hex(getContentBytes(text, input_charset));

if (mysign.equals(sign)) {

return true;

} else {

return false;

}

}

/**

* @param content

* @param charset

* @return

* @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);

}

}

/**

* 生成6位或10位随机数 param codeLength(多少位)

* @return

*/

public static String createCode(int codeLength) {

String code = "";

for (int i = 0; i < codeLength; i++) {

code += (int) (Math.random() * 9);

}

return code;

}

@SuppressWarnings("unused")

private static boolean isValidChar(char ch) {

if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))

return true;

if ((ch >= 0x4e00 && ch <= 0x7fff) || (ch >= 0x8000 && ch <= 0x952f))

return true;// 简体中文汉字编码

return false;

}

/**

* 除去数组中的空值和签名参数

* @param sArray 签名参数组

* @return 去掉空值与签名参数后的新签名参数组

*/

public static Map paraFilter(Map sArray) {

Map result = new HashMap<>();

if (sArray == null || sArray.size() <= 0) {

return result;

}

for (String key : sArray.keySet()) {

String value = sArray.get(key);

if (value == null || value.equals("") || key.equalsIgnoreCase("sign")

|| key.equalsIgnoreCase("sign_type")) {

continue;

}

result.put(key, value);

}

return result;

}

/**

* 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串

* @param params 需要排序并参与字符拼接的参数组

* @return 拼接后字符串

*/

public static String createLinkString(Map params) {

List 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 requestUrl 请求地址

* @param requestMethod 请求方法

* @param outputStr 参数

*/

public static String httpRequest(String requestUrl,String requestMethod,String outputStr){

logger.warn("请求报文:"+outputStr);

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();

}

logger.warn("返回报文:"+buffer.toString());

return buffer.toString();

}

/**

* POST请求

* @param url 请求url

* @param xmlParam 请求参数

* @param apiclient 证书

* @param mch_id 商户号

* @return

* @throws Exception

*/

public static String post(String url, String xmlParam,String apiclient,String mch_id) throws Exception {

logger.warn("请求报文:"+xmlParam);

StringBuilder sb = new StringBuilder();

try {

KeyStore keyStore = KeyStore.getInstance("PKCS12");

FileInputStream instream = new FileInputStream(new File(apiclient));

try {

keyStore.load(instream, mch_id.toCharArray());

} finally {

instream.close();

}

// 证书

SSLContext sslcontext = SSLContexts.custom()

.loadKeyMaterial(keyStore, mch_id.toCharArray()).build();

// 只允许TLSv1协议

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(

sslcontext, new String[]{"TLSv1"}, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);

//创建基于证书的httpClient,后面要用到

CloseableHttpClient client = HttpClients.custom().setSSLSocketFactory(sslsf).build();

HttpPost httpPost = new HttpPost(url);//退款接口

StringEntity reqEntity = new StringEntity(xmlParam,"UTF-8");

// 设置类型

reqEntity.setContentType("application/x-www-form-urlencoded");

httpPost.setEntity(reqEntity);

CloseableHttpResponse response = client.execute(httpPost);

try {

HttpEntity entity = response.getEntity();

System.out.println(response.getStatusLine());

if (entity != null) {

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));

String text = "";

while ((text = bufferedReader.readLine()) != null) {

sb.append(text);

}

}

EntityUtils.consume(entity);

} catch (Exception e) {

e.printStackTrace();

} finally {

try {

response.close();

} catch (IOException e) {

e.printStackTrace();

}

}

} catch (KeyStoreException e) {

e.printStackTrace();

} catch (ClientProtocolException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

} catch (Exception e) {

e.printStackTrace();

}

logger.warn("返回报文:"+sb.toString());

return sb.toString();

}

public static String urlEncodeUTF8(String source){

String result=source;

try {

result=java-.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 = String2Inputstream(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 String2Inputstream(String str) {

return new ByteArrayInputStream(str.getBytes());

}

}

controller:

package com.jch.mng.controller;

import com.jch.boot.component.CommonInfo;

import com.jch.boot.component.Result;

import com.jch.boot.component.ServiceCommonInfo;

import com.jch.mng.dto.input.gzh.WxPayDto;

import com.jch.mng.service.WxPayService;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

/**

* Created by xxs on 2021/7/30 10:54

*

* @Version 2.9

*/

@RestController

@RequestMapping("/wxPay")

public class WxPayController {

@Autowired

private WxPayService payService;

/**

* @Author: xxs

* @param dto

* @param request

* @Date: 2021/7/30 11:55

* @Version: 2.9

* @Return: com.jch.boot.component.Result

*/

@PostMapping("/pay")

public Result pay(@RequestBody WxPayDto dto, HttpServletRequest request) throws Exception {

ServiceCommonInfo result = payService.pay(dto,request);

return CommonInfo.controllerBack(result);

}

/**

* @Author: xxs

* @param request

* @param response

* @Date: 2021/7/30 11:55

* @Description: 支付回调

* @Version: 2.9

* @Return: void

*/

@PostMapping("/notify")

public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception {

payService.notify(request,response);

}

}

service接口:

package com.jch.mng.service;

import com.jch.boot.component.ServiceCommonInfo;

import com.jch.mng.dto.input.gzh.WxPayDto;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

/**

* Created by xxs on 2021/7/30 9:56

*

* @Description

* @Version 2.9

*/

public interface WxPayService {

ServiceCommonInfo pay(WxPayDto dto, HttpServletRequest request) throws Exception;

void notify(HttpServletRequest request, HttpServletResponse response) throws Exception;

}

接口实现:

package com.jch.mng.service.impl;

import com.alibaba.fastjson.JSON;

import com.jc.utils.util.CommUtils;

import com.jch.boot.component.ServiceCommonInfo;

import com.jch.mng.constant.WeChatConstants;

import com.jch.mng.dto.input.gzh.WxPayDto;

import com.jch.mng.service.WxPayService;

import com.jch.mng.utils.DoubleUtil;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.stereotype.Service;

import javax.servlet.ServletInputStream;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.BufferedOutputStream;

import java.io.BufferedReader;

import java.io.InputStreamReader;

import java.util.HashMap;

import java.util.Map;

/**

* Created by xxs on 2021/7/30 9:56

*

* @Description

* @Version 2.9

*/

@Service

public class WxPayServiceImpl implements WxPayService {

public String appId;

public String mch_id;

public String notify_url;

public String key;

@Value("${gzh.appid}")

public void setAppId(String appId) {

this.appId = appId;

}

@Value("${wxPay.mchId}")

public void setMch_id(String mch_id) {

this.mch_id = mch_id;

}

@Value("${wxPay.notifyUrl}")

public void setNotify_url(String notify_url) {

this.notify_url = notify_url;

}

@Value("${wxPay.key}")

public void setKey(String key) {

this.key = key;

}

private static Logger logger = LoggerFactory.getLogger(WxPayServiceImpl.class);

/**

* @Author: xxs

* @param dto

* @param request

* @Date: 2021/7/30 11:01

* @Description: 微信支付

* @Version: 2.9

* @Return: com.jch.boot.component.ServiceCommonInfo

*/

@Override

public ServiceCommonInfo pay(WxPayDto dto, HttpServletRequest request) throws Exception {

String openid = dto.getOpenid();

String outTradeNo = dto.getOutTradeNo();

String body = dto.getBody();

Double totalFee = dto.getTotalFee();

String nonce_str = CommUtils.getRandomStringByLength(32);

String spbill_create_ip = CommUtils.getIpAddr(request);

Map packageParams = new HashMap<>();

packageParams.put("appid", appId);

packageParams.put("mch_id",mch_id);

packageParams.put("nonce_str", nonce_str);

packageParams.put("body", body);

packageParams.put("out_trade_no", outTradeNo);

double t = DoubleUtil.parseDouble(totalFee);//保留两位小数

int aDouble = Integer.parseInt(new java.text.DecimalFormat("0").format(t*100));

packageParams.put("total_fee", aDouble+"");

packageParams.put("spbill_create_ip", spbill_create_ip);

packageParams.put("notify_url", notify_url);

packageParams.put("trade_type","JSAPI");

packageParams.put("openid", openid);

packageParams = CommUtils.paraFilter(packageParams);

String prestr = CommUtils.createLinkString(packageParams);

String sign = CommUtils.sign(prestr, key, "utf-8").toUpperCase();

logger.info("统一下单请求签名:" + sign );

String xml = "" + "" + appId + ""

+ "

+ "" + mch_id + ""

+ "" + nonce_str + ""

+ "" + notify_url+ ""

+ "" + openid + ""

+ "" + outTradeNo + ""

+ "" + spbill_create_ip + ""

+ "" + aDouble+"" + ""

+ "" + "JSAPI" + ""

+ "" + sign + ""

+ "";

String result = CommUtils.httpRequest(WeChatConstants.unifiedOrderUrl, "POST", xml);

Map map = CommUtils.doXMLParse(result);

Object return_code = map.get("return_code");

logger.info("统一下单返回return_code:" + return_code );

if(return_code == "SUCCESS" || return_code.equals(return_code)){

Map resultMap=new HashMap();

String prepay_id = (String) map.get("prepay_id");

resultMap.put("appId", appId);

Long timeStamp = System.currentTimeMillis() / 1000;

resultMap.put("timeStamp", timeStamp + "");

resultMap.put("nonceStr", nonce_str);

resultMap.put("package", "prepay_id=" + prepay_id);

resultMap.put("signType", "MD5");

logger.info("参与paySign签名数据, 入参:{}", JSON.toJSONString(resultMap));

String linkString = CommUtils.createLinkString(resultMap);

String paySign = CommUtils.sign(linkString, key, "utf-8").toUpperCase();

logger.info("获取到paySign:"+paySign);

resultMap.put("paySign", paySign);

return ServiceCommonInfo.success("ok", resultMap);

}

return ServiceCommonInfo.serviceFail("支付失败", null);

}

/**

* @Author: xxs

* @param request

* @param response

* @Date: 2021/7/31 15:17

* @Description: 微信支付回调

* @Version: 2.9

* @Return: void

*/

@Override

public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception {

logger.info("进入支付回调啦啦啦啦*-*");

String resXml = "";

BufferedReader br = new BufferedReader(new InputStreamReader((ServletInputStream) request.getInputStream()));

String line = null;

StringBuilder sb = new StringBuilder();

while ((line = br.readLine()) != null) {

sb.append(line);

}

br.close();

String notityXml = sb.toString();

logger.info("支付回调返回数据:"+notityXml);

Map map = CommUtils.doXMLParse(notityXml);

Object returnCode = map.get("return_code");

Object result_code = map.get("result_code");

if ("SUCCESS".equals(returnCode) && "SUCCESS".equals(result_code)) {

Map validParams = CommUtils.paraFilter(map); //回调验签时需要去除sign和空值参数

String validStr = CommUtils.createLinkString(validParams);//把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串

String sign = CommUtils.sign(validStr, key , "utf-8").toUpperCase();//拼装生成服务器端验证的签名

logger.info("支付回调生成签名:"+sign);

String transaction_id = (String) map.get("transaction_id");

String order_no = (String) map.get("out_trade_no");

String time_end = (String) map.get("time_end");

String total_fee = (String) map.get("total_fee");

//签名验证,并校验返回的订单金额是否与商户侧的订单金额一致

if (sign.equals(map.get("sign"))) {

logger.info("支付回调验签通过");

//通知微信服务器已经支付成功

resXml = WeChatConstants.SUCCESSxml;

} else {

logger.info("微信支付回调失败!签名不一致");

}

}else{

resXml = WeChatConstants.ERRORxml;

}

System.out.println(resXml);

logger.info("微信支付回调返回数据:"+resXml);

logger.info("微信支付回调数据结束");

BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());

out.write(resXml.getBytes());

out.flush();

out.close();

}

}

前端页面:

body:

outTradeNo:

totalFee:

openid:

&nbspNTUer;访问前端页面记得加依赖:

org.springframework.boot

spring-boot-starter-thymeleaf

访问页面需要写控制类:

package com.jch.mng.controller;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletResponse;

/**

* Created by xxs on 2021/7/31 12:29

*

* @Description

* @Version 2.9

*/

@Controller

public class TestPageController {

@RequestMapping("/wxPayTest")

public String test(HttpServletResponse response) {

return "wxPay";

}

}

运行项目访问。

部署项目到服务器,用手机访问即可拉起支付。

附:名词解释

商户号:微信支付分配的商户号。支付审核通过后,申请人邮箱会收到腾讯下发的开户邮件, 邮件中包含商户平台的账号、密码等重要信息。

appid:商户通过微信管理后台,申请服务号、订阅号、小程序或APP应用成功之后,微信会为每个应用分配一个唯一标识id。

微信管理后台:微信有很多管理平台,容易混淆,我们主要关注下面三个平台:

1. 微信公众平台 微信公众账号申请入口和管理后台。商户可以在公众平台提交基本资料、业务资料、财务资料申请开通微信支付功能。帐号分类:服务号、订阅号、小程序、企业微信(也叫企业号,类似于企业OA)。

2. 微信商户平台 微信支付相关的商户功能集合,包括参数配置、支付数据查询与统计、在线退款、代金券或立减优惠运营等功能。

3. 微信开放平台 商户APP接入微信支付开放接口的申请入口,通过此平台可申请微信APP支付。

签名:商户后台和微信支付后台根据相同的密钥和算法生成一个结果,用于校验双方身份合法性。签名的算法 由微信支付制定并公开,常用的签名方式有:MD5、SHA1、SHA256、HMAC等。

密钥:作为签名算法中的盐,需要在微信平台及商户业务系统各存一份,要妥善保管。 key设置路径:微信商户平台(http://pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置。

总结

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:flutter云信(flutter 融云im)
下一篇:特斯拉车载物联网所用频段(特斯拉车载物联网所用频段有哪些)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~