情景:最近有个微信小程序支付的项目,在回调返回时踩坑,特此在这记录一下。
需求流程:小程序用户钱包充值→调用小程序官方小程序支付接口生成于支付界面→支付完成后回调提示支付成功回写相关参数到用户表中
1.小程序:点击支付按钮,请求后台接口,返回5个必要参数(用于之后调wx.requestPayment(OBJECT))。 2.JAVA: 调用统一下单接口,生成微信预支付订单,并返回结果。 3.小程序:接收到5个参数后,调用wx.requestPayment(OBJECT),此时小程序唤起了输入密码的支付弹窗,我们可以选择关闭弹窗和支付,然后根据情况跳转到不同页面。 4.JAVA: 当在小程序内支付完后,此时微信服务器会有规律性给你的支付回调地址发起请求,通知你支付结果。
官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1 想了解的可以去看看
准备工作:提前申请好小程序支付密钥和证书相关信息(PS:这里就不啰嗦啦!)
1.在application.yml配置
wx:
mini:
appid: xxxx
secret:xxxx
## 商品名称
body: 小程序支付(测试商品)
## 微信商户号
mch_id: xxxx
## 商户秘钥
mch_key: xxxx
#本地 外部支付成功跳转路径(需要外部服务可访问)
base_notify_url: https://tishang.cn.utools.club
notify_url1: ${wx.mini.base_notify_url}/api/wx/pay/notifyWalletUrl
#证书地址 服务器配置存放路径 本地测试就放到本地文件
apiclient: D:/usr/server/pkcs12/apiclient_cert.p12
2.支付用到的实体如下:
微信用户实体:WxUser
package com.seeker.wxpayment.wx.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.seeker.wxpayment.common.base.DataEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
/**
* 微信用户信息
*
* @author seeker
* @since 2021-05-11
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class WxUser extends DataEntity<WxUser> {
private static final long serialVersionUID = 1L;
/**
* id
*/
private String id;
/**
* OpenId
*/
private String openid;
/**
* 用户名称
*/
private String userName;
/**
* 性别 0 男 1女
*/
private String sex;
/**
* 上传头像
*/
private String imageUrl;
/**
* 登录名
*/
private String loginName;
/**
* 手机
*/
private String mobile;
/**
* 登录密码
*/
private String password;
/**
* 钱包支付密码
*/
private String payPassword;
/**
* 会员类型 0非会员1会员
*/
private String memberType;
/**
* 会员余额
*/
private BigDecimal balanceAmount;
/**
* 联系人
*/
private String contact;
@Override
public String getPkVal() {
return this.id;
}
@Override
public void setPkVal(String pkVal) {
this.id = pkVal;
}
}
两个共用的实体DataEntity和Entity(注:可以不用)
import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
@Data
public abstract class DataEntity<T extends Model> extends BaseEntity<T> {
/**
* 创建者
*/
@JSONField(serialize = false)
@TableField("create_by")
protected String createBy;
/**
* 创建日期
*/
@JSONField(serialize = true, format = "yyyy-MM-dd HH:mm:ss")
@TableField("create_date")
protected Date createDate;
/**
* 更新者
*/
@JSONField(serialize = false)
@TableField("update_by")
protected String updateBy;
/**
* 更新日期
*/
@JSONField(serialize = true, format = "yyyy-MM-dd HH:mm:ss")
@TableField("update_date")
protected Date updateDate;
/**
* 状态(0正常 1删除 2停用)
*/
@JSONField(serialize = false)
@TableField("del_flag")
protected String delFlag;
/**
* 备注
*/
protected String remarks;
@TableField(exist = false)
private String orderBy;
/**
* 插入之前执行方法,需要手动调用
*/
@Override
public void preInsert() {
String uuid = IdWorker.getIdStr();
setPkVal(uuid);
this.updateDate = new Date();
this.createDate = this.updateDate;
}
/**
* 更新之前执行方法,需要手动调用
*/
@Override
public void preUpdate() {
this.updateDate = new Date();
this.createDate = this.updateDate;
}
}
import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
import java.util.Map;
/**
* Entity支持类
*/
@Data
public abstract class BaseEntity<T extends Model> extends Model {
private static final long serialVersionUID = 1L;
/**
* 获取主键
*
* @return
*/
public abstract String getPkVal();
/**
* 设置主键
*
* @return
*/
public abstract void setPkVal(String pkVal);
/**
* 插入之前执行方法,子类实现
*/
public abstract void preInsert();
/**
* 更新之前执行方法,子类实现
*/
public abstract void preUpdate();
/**
* 自定义SQL(SQL标识,SQL内容)
*/
@JSONField(serialize = false)
@TableField(exist = false)
protected Map<String, String> sqlMap;
/**
* mybatis-plus分页参数
*/
@JSONField(serialize = false)
@TableField(exist = false)
private IPage<T> page;
public Page<T> getPage() {
return new Page<>(pageNo, pageSize);
}
/**
* 当前页码
*/
@TableField(exist = false)
private long pageNo = 1L;
/**
* 每页条数
*/
@TableField(exist = false)
private long pageSize = 10L;
/**
* 删除标记(0:正常;1:删除;)
*/
public static final String DEL_FLAG_NORMAL = "0";
public static final String DEL_FLAG_DELETE = "1";
}
充值消费记录实体:WxUserConsumeLog
package com.seeker.wxpayment.wx.entity;
import com.seeker.wxpayment.common.base.DataEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 微信用户消费记录
*
* @author seeker
* @since 2021-0-11
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class WxUserConsumeLog extends DataEntity<WxUserConsumeLog> {
private static final long serialVersionUID = 1L;
/**
* id
*/
private String id;
/**
* 微信用户ID wx_user中id
*/
private String wxUserId;
/**
* 消费类型 0 开通VIP 1 钱包充值 2钱包支付
*/
private String consumeType;
/**
* 消费内容
*/
private String consumeContent;
/**
* 支付金额
*/
private String payAmount;
@Override
public String getPkVal() {
return this.id;
}
@Override
public void setPkVal(String pkVal) {
this.id = pkVal;
}
}
传参实体:Pay
import com.seeker.common.base.DataEntity;
import java.io.Serializable;
public class Pay extends DataEntity<Pay> {
private String id;
private String orderId; //订单ID
private String openid; //
private String code;
private double payPrice; //支付金额
private String type; //支付方式 1 VIP 2钱包充值 3 订单支付
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
public double getPayPrice() {
return payPrice;
}
public void setPayPrice(double payPrice) {
this.payPrice = payPrice;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public String getPkVal() {
return this.id;
}
@Override
public void setPkVal(String uuid) {
this.id = uuid;
}
}
3、创建一个支付调用Controller层:WXPayController
package com.seeker.advertising.api;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.seeker.advertising.auth.WxLoginUser;
import com.seeker.advertising.auth.WxMiniTokenService;
import com.seeker.advertising.order.entity.OrderInfo;
import com.seeker.advertising.order.entity.OrderPayRefund;
import com.seeker.advertising.order.entity.OutSuccess;
import com.seeker.advertising.order.entity.Pay;
import com.seeker.advertising.order.service.OrderInfoService;
import com.seeker.advertising.order.service.OrderPayInfoService;
import com.seeker.advertising.order.service.OrderPayRefundService;
import com.seeker.advertising.wx.entity.WxUser;
import com.seeker.advertising.wx.entity.WxUserInfo;
import com.seeker.advertising.wx.service.WxUserConsumeLogService;
import com.seeker.advertising.wx.service.WxUserCouponService;
import com.seeker.advertising.wx.service.WxUserService;
import com.seeker.common.utils.*;
import lombok.extern.java.Log;
import org.apache.commons.lang3.StringUtils;
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.*;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @Title:
* @Description: 微信小程序支付
* @Author:seeker
* @Since:2021年5月20日
* @Version:1.1.0
*/
@Log
@RestController
@RequestMapping("/api/wx/pay")
public class WXPayController {
@Value("${wx.mini.appid}")
private String wxspAppid;
@Value("${wx.mini.secret}")
private String wxspSecret;
@Value("${wx.mini.mch_id}")
private String mch_id;
@Value("${wx.mini.mch_key}")
private String mch_key;
@Value("${wx.mini.body}")
private String body;
@Value("${wx.mini.notify_url}")
private String notify_url;
@Value("${wx.mini.apiclient}")
private String apiclient;
@Autowired
private WxMiniTokenService wxMiniTokenService;
@Autowired
private WxUserService wxUserService;
@Autowired
private OrderPayInfoService OrderPayInfoService;
@Autowired
private OrderInfoService orderInfoService;
protected Logger logger = LoggerFactory.getLogger(getClass());
/**
** 大体流程分析:
1.小程序:点击支付按钮,请求后台接口,返回5个必要参数(用于之后调wx.requestPayment(OBJECT))。
2.JAVA: 调用统一下单接口,生成微信预支付订单,并返回结果。
3.小程序:接收到5个参数后,调用wx.requestPayment(OBJECT),此时小程序唤起了输入密码的支付弹窗,我们可以选择关闭弹窗和支付,然后根据情况跳转到不同页面。
4.JAVA: 当在小程序内支付完后,此时微信服务器会有规律性给你的支付回调地址发起请求,通知你支付结果。
*/
/**
* 【1】调用微信小程序支付
* 1.调用微信小程序API
* 2.1支付成功后设置钱包余额,返回成功状态
* 2.2支付失败,返回错误状态
*/
@RequestMapping(value = "variedPayInfo")
public R variedPayInfo(@RequestBody(required = false) Pay pay, HttpServletRequest request, HttpServletResponse response) {
R r = R.ok();
try {
WxLoginUser loginUser = wxMiniTokenService.getLoginUser(ServletUtils.getRequest());
//获取openid
String openid = "";
//判断是否登陆
if (null == loginUser || StringUtils.isBlank(loginUser.getUser().getOpenid())) {
// 授权(必填)
String grant_type = "authorization_code";
// 1、向微信服务器 使用登录凭证 code 获取 session_key 和 openid
// 请求参数
String params = "appid=" + wxspAppid + "&secret=" + wxspSecret + "&js_code=" + pay.getCode()
+ "&grant_type=" + grant_type;
// 发送请求
String sr = HttpRequestUtils.sendGet("https://api.weixin.qq.com/sns/jscode2session", params);
WxUserInfo wxUserInfo = JSON.parseObject(sr, new TypeReference<WxUserInfo>() {
});
log.info("wxUserInfo === " + wxUserInfo);
openid = wxUserInfo.getOpenid();
} else {
openid = loginUser.getUser().getOpenid();
}
//组装参数,用户生成统一下单接口的签名
Map<String, String> paraMap = new HashMap<String, String>();
paraMap.put("appid", wxspAppid);
paraMap.put("body", body);
paraMap.put("mch_id", mch_id);
paraMap.put("nonce_str", WXPayUtil.create_nonce_str()); //随机字符串
paraMap.put("openid", openid);
if (StringUtils.isNotEmpty(pay.getOrderId())) {
paraMap.put("out_trade_no", pay.getOrderId()); //商户订单号
} else {
paraMap.put("out_trade_no", "DT" + IdWorker.getIdStr()); //商户订单号
}
paraMap.put("spbill_create_ip", WXPayUtil.getAddrIp(request)); //ip地址
double totalFee = 0;
//支付方式 1 VIP 2钱包充值 3 订单支付
if (StringUtils.isNotEmpty(pay.getType())) {
if (pay.getType().equals("2")) {
//totalFee = pay.getPayPrice();//钱包充值
totalFee = 0.01 * 100; //测试 使用1分钱
paraMap.put("notify_url", notify_url);
}
}
int money = (int) totalFee;
// int money = (int) AmountUtils.Amount.PAY_AMOUNT * 100;
paraMap.put("total_fee", String.valueOf(money)); //标价金额
paraMap.put("trade_type", ConstantUtils.JSAPI); //交易类型(小程序取值如下:JSAPI)
//保存支付信息(支付成功才会回调,关闭支付弹窗不会回调)
// paraMap.put("notify_url", notify_url);//支付成功后的回调地址 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。
//MD5运算生成签名,这里是第一次签名,用于调用统一下单接口
String sign = WXPayUtil.getSign(paraMap, mch_key);
paraMap.put("sign", sign);
//map转化为xml格式
String requestXml = WXPayUtil.mapToXml(paraMap);
//调用统一下单接口,并接受返回的结果
String resultXml = HttpRequestUtils.httpsRequestWeiXin(ConstantUtils.UNIFIED_ORDER_URL, ConstantUtils.REQUESTMETHODPOST, requestXml);
// 将解析结果存储在HashMap中
Map<String, String> param = isResponseSignatureValid(resultXml, mch_key);
String return_code = param.get("return_code");//返回状态码
String result_code = param.get("result_code");//返回状态码
//返回给小程序端需要的参数(字段必须 驼峰模式 严格按照小程序支付文档)
Map<String, String> payMap = new HashMap<String, String>();
if (return_code.equals("SUCCESS") && return_code.equals(result_code)) {
//设置订单基础参数
if (StringUtils.isNotEmpty(pay.getOrderId())) {
OrderInfo orderInfo = new OrderInfo();
orderInfo.setId(pay.getOrderId());
orderInfo.setIndustry(pay.getIndustry());
orderInfo.setBrandName(pay.getBrandName());
orderInfo.setRemarks(pay.getRemarks());
orderInfoService.update(orderInfo);
}
payMap.put("appId", wxspAppid);
payMap.put("timeStamp", WXPayUtil.create_timestamp()); //这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误 //拼接签名需要的参数
payMap.put("nonceStr", WXPayUtil.create_nonce_str()); //生成签名的随机串
//prepay_id ==>微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时
payMap.put("package", "prepay_id=" + param.get("prepay_id")); //订单详情扩展字符串
payMap.put("signType", ConstantUtils.SIGNTYPE); //签名方式 md5
String paySign = WXPayUtil.getSign(payMap, mch_key); //签名
payMap.put("paySign", paySign);
r.put("map", payMap);
r.put("code", 0);
r.put("msg", "预支付成功!");
} else {
r.put("code", 1);
r.put("msg", param.get("err_code_des"));
}
} catch (Exception e) {
e.printStackTrace();
r = R.error();
}
return r;
}
/**
* 【2】微信支付通知(微信回调)钱包充值费用
*
* @param request
* @param response
* @return outResult 支付是否成功
*/
@RequestMapping(value = "notifyWalletUrl", method = RequestMethod.POST)
public void notifyWalletUrl(HttpServletRequest request, HttpServletResponse response) {
log.info("微信支付——异步回调开始");
OutSuccess outResult = new OutSuccess();
String resXml = "";
try {
ServletInputStream inputStream = request.getInputStream();
Map<String, String> params = WXPayUtil.parseXml(inputStream);
log.info(params.toString());
String returnCode = params.get("return_code");
String resultCode = params.get("result_code");
String returnMsg = params.get("return_msg");
log.info("returnMsg:" + returnMsg);
if ("SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode)) {
String sign = params.get("sign");
// 签名sign不需要参加移除
params.remove("sign");
String signResult = WXPayUtil.getSign(params, mch_key);
//通过签名验证的签名结果,可认为是微信官方的通知
outResult = OrderPayInfoService.saveWechatPayWallet(params, sign, signResult);
if ("SUCCESS".equals(outResult.getReturn_code())) {
//支付成功
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>";
log.info("打印出成功回调结果---->resXml" + resXml);
} else {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[签名错误]></return_msg>" + "</xml>";
logger.error("签名错误,xml中签名:" + sign + ",校验后的签名:" + signResult);
}
} else {
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[报文为空]></return_msg>" + "</xml>";
logger.info("支付失败:" + resXml);
log.info("获取数据失败,错误代码:" + params.get("err_code") + ",错误描述:" + params.get("err_code_des"));
}
logger.info("【小程序支付回调响应】 响应内容:\n" + resXml);
response.getWriter().print(resXml);
} catch (IOException e) {
e.printStackTrace();
}
}
}
数据公共类:ConstantUtils
package com.seeker.common.utils;
/**
* Created by seeker on 2021/5/20
*/
public class ConstantUtils {
/**
* 微信
*/
public final static String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
public final static String REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund";
//微信支付类型
public final static String JSAPI = "JSAPI";
public static final String FAIL = "FAIL";
public static final String SUCCESS = "SUCCESS";
public static final String HMACSHA256 = "HMAC-SHA256";
public final static String SIGNTYPE = "MD5";
public final static String REQUESTMETHODPOST = "POST";
public final static String REQUESTMETHODGET = "GET";
//随机字符串
public static final String RANDOM_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
}
微信请求封装类:HttpRequestUtils
package com.seeker.common.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.*;
import java.net.ConnectException;
import java.net.URL;
import java.net.URLConnection;
public class HttpRequestUtils {
protected static Logger logger = LoggerFactory.getLogger("HttpRequestUtils");
/**
* 向指定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;
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()) { logger.info(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) {
logger.info("发送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()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
logger.info("发送 POST 请求出现异常!" + e);
e.printStackTrace();
}
// 使用finally块来关闭输出流、输入流
finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
return result;
}
/**
* 发送https请求
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return 返回微信服务器响应的信息
*/
public static String httpsRequestWeiXin(String requestUrl, String requestMethod, String outputStr) {
try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = {new MyX509TrustManager()};
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);
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 (ConnectException ce) {
logger.error("连接超时:{}", ce);
} catch (Exception e) {
logger.error("https请求异常:{}", e);
}
return null;
}
}
传递和接收数据封装:WXPayUtil
package com.seeker.common.utils;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.naming.NoNameCoder;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.util.*;
public class WXPayUtil {
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
//System.out.println("==========================xml2:" + strXML);
try {
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) {
// do nothing
}
return data;
} catch (Exception ex) {
WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
throw ex;
}
}
/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
//System.out.println("==============================xml:" + output);
return output;
}
/**
* 生成带有 sign 的 XML 格式字符串
*
* @param data Map类型数据
* @param key API密钥
* @return 含有sign字段的XML
*/
public static String generateSignedXml(final Map<String, String> data, String key) throws Exception {
return generateSignedXml(data, key, WXPayConstants.SignType.MD5);
}
/**
* 生成带有 sign 的 XML 格式字符串
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名类型
* @return 含有sign字段的XML
*/
public static String generateSignedXml(final Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
String sign = generateSignature(data, key, signType);
data.put(WXPayConstants.FIELD_SIGN, sign);
return mapToXml(data);
}
/**
* 判断签名是否正确
*
* @param xmlStr XML格式数据
* @param key API密钥
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
Map<String, String> data = xmlToMap(xmlStr);
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key).equals(sign);
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
*
* @param data Map类型数据
* @param key API密钥
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key) throws Exception {
return isSignatureValid(data, key, WXPayConstants.SignType.MD5);
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名方式
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return generateSignature(data, key, signType).equals(sign);
}
/**
* 生成签名
*
* @param data 待签名数据
* @param key API密钥
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key) throws Exception {
return generateSignature(data, key, WXPayConstants.SignType.MD5);
}
/**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @param signType 签名方式
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key, WXPayConstants.SignType signType) throws Exception {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals(WXPayConstants.FIELD_SIGN)) {
continue;
}
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("key=").append(key);
if (WXPayConstants.SignType.MD5.equals(signType)) {
return MD5(sb.toString()).toUpperCase();
}
else if (WXPayConstants.SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
}
else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
/**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
public static String MD5(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 生成 HMACSHA256
* @param data 待处理数据
* @param key 密钥
* @return 加密结果
* @throws Exception
*/
public static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 日志
* @return
*/
public static Logger getLogger() {
Logger logger = LoggerFactory.getLogger("wxpay java sdk");
return logger;
}
/**
* 获取当前时间戳,单位秒
* @return
*/
public static long getCurrentTimestamp() {
return System.currentTimeMillis()/1000;
}
/**
* 获取当前时间戳,单位毫秒
* @return
*/
public static long getCurrentTimestampMs() {
return System.currentTimeMillis();
}
/**
* 生成 uuid, 即用来标识一笔单,也用做 nonce_str
* @return
*/
public static String generateUUID() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
public static String create_nonce_str() {
String chars = ConstantUtils.RANDOM_CHARS;
String res = "";
for (int i = 0; i < 16; i++) {
Random rd = new Random();
res += chars.charAt(rd.nextInt(chars.length() - 1));
}
return res;
}
public static String getAddrIp(HttpServletRequest request) {
return request.getRemoteAddr();
}
public static String create_timestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
public static String getSign(Map<String, String> params, String paternerKey) throws UnsupportedEncodingException {
String string1 = createSign(params, false);
String stringSignTemp = string1 + "&key=" + paternerKey;
String signValue = DigestUtils.md5Hex(stringSignTemp).toUpperCase();
return signValue;
}
/**
* 将流的xml数据转成map
* @param inputStream
* @return
*/
public static Map<String, String> parseXml(InputStream inputStream ) {
Map<String, String> map = new HashMap<String, String>();
try {
SAXReader reader = new SAXReader();
// 读取输入流
Document document = reader.read(inputStream);
// 得到xml根元素
Element rootElement = document.getRootElement();
// 得到根元素所有的子元素
@SuppressWarnings("unchecked")
List<Element> elements = rootElement.elements();
// 遍历所有子节点
for (Element e : elements) {
if (StringUtils.isNotEmpty(e.getText())) {// 去除非空的数据
map.put(e.getName(), e.getText());
}
}
inputStream.close();
inputStream = null;
} catch (DocumentException e) {
} catch (IOException e) {
}
return map;
}
/**
* 自动包装<![CDATA]>代码块
* @param o
* @return
*/
public static String toXmlWithCDATA(Object o){
escapeXstream.alias("xml", o.getClass());
return escapeXstream.toXML(o);
}
/**
* 支持CDATA块
*
*/
private static XStream escapeXstream = new XStream(new XppDriver() {
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out,new NoNameCoder()) {
public void startNode(String name, @SuppressWarnings("rawtypes") Class clazz) {
super.startNode(name, clazz);
}
protected void writeText(QuickWriter writer, String text) {
writer.write("<![CDATA[");
writer.write(text);
writer.write("]]>");
}
};
}
});
/**
* 构造签名
* @param params
* @param encode
* @return
* @throws UnsupportedEncodingException
*/
public static String createSign(Map<String, String> params, boolean encode) throws UnsupportedEncodingException {
Set<String> keysSet = params.keySet();
Object[] keys = keysSet.toArray();
Arrays.sort(keys);
StringBuffer temp = new StringBuffer();
boolean first = true;
for (Object key : keys) {
if (first) {
first = false;
} else {
temp.append("&");
}
temp.append(key).append("=");
Object value = params.get(key);
String valueString = "";
if (null != value) {
valueString = value.toString();
}
if (encode) {
temp.append(URLEncoder.encode(valueString, "UTF-8"));
} else {
temp.append(valueString);
}
}
return temp.toString();
}
}
4、支付成功后业务处理及返回消息:OrderPayInfoService处理
/**
* 保存微信支付钱包信息
*
* @param params
* @param sign
* @param signResult
* @return
*/
@Transactional(readOnly = false)
public OutSuccess saveWechatPayWallet(Map<String, String> params, String sign, String signResult) {
OutSuccess outResult;
BigDecimal oldAmount;
BigDecimal allAmount;
if (sign.equals(signResult)) {
logger.info("微信——状态验证成功,同步本地订单状态");
String transaction_id = params.get("transaction_id");// 微信交易号
String totalFee = params.get("total_fee");
logger.info("微信交易号=====" + transaction_id);
String openid = params.get("openid");//
WxUser wxUser = wxUserMapper.getByOpenid(openid);
//设置用户余额
if (null == wxUser.getBalanceAmount() || "".equals(wxUser.getBalanceAmount()) || "0".equals(wxUser.getBalanceAmount()) || "0.00".equals(wxUser.getBalanceAmount())) {
oldAmount = new BigDecimal(0);
} else {
oldAmount = wxUser.getBalanceAmount();
}
String amount = CalculateUtils.CalculateUtil1(new BigDecimal(totalFee), new BigDecimal(100));
BigDecimal newAmount = new BigDecimal(amount);
allAmount = newAmount.add(oldAmount);
wxUser.setBalanceAmount(allAmount);//设置余额
wxUser.preUpdate();
wxUserMapper.updateById(wxUser);
logger.info("微信wxuser=====" + wxUser);
//添加一条消费记录
WxUserConsumeLog wxUserConsumeLog = new WxUserConsumeLog();
//微信用户ID
wxUserConsumeLog.setWxUserId(wxUser.getId());
//微信支付金额
wxUserConsumeLog.setPayAmount(String.valueOf(amount));
//消费类型 0 开通VIP 1 钱包充值
wxUserConsumeLog.setConsumeType("1");
wxUserConsumeLog.setConsumeContent("用户在【" +
DateUtils.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss")
+ "】钱包充值,本次充值金额为:" + amount + "元,充值之前余额为:" + oldAmount + "元,充值之后余额为:" + allAmount + "元");
wxUserConsumeLog.preInsert();
wxUserConsumeLogMapper.insert(wxUserConsumeLog);
logger.info("wxUserConsumeLog=====" + wxUserConsumeLog);
outResult = new OutSuccess("SUCCESS", "OK");
//outResult = WXPayUtil.toXmlWithCDATA(success);
logger.info("微信--同步本地订单支付成功");
} else {
outResult = new OutSuccess("FAIL", "ERROR_SIGN");
logger.error("签名错误,xml中签名:" + sign + ",校验后的签名:" + signResult);
// outResult = WXPayUtil.toXmlWithCDATA(success);
}
return outResult;
}
总结:微信小程序支付,需要先把微信小程序相关的密钥和证书申请好,然后调用预支付页面,最后支付完成调用业务层接口回写相关数据就欧了。注意的是 支付成功回调路径和返回参数要严格按照微信小程序官方文档,不然就会找不到回调路径以及会出现多次回调问题。
OK 到这里就结束了