微信支付之扫码支付与小程序支付
前言:最近的需求中,频繁出现微信支付功能的开发,于是研读了微信官方开发文档以及相关代码做了以下总结,并记录在此,以备不时之需。如有不足之处,欢迎批评指正。
微信官方开发文档
扫码支付
模式二:本文着重介绍扫码支付的模式二,其他情况以此类推,主要区别在统一下单前的步骤。
- 商户后台系统先调用微信支付的统一下单接口。
- 微信后台系统返回链接参数code_url。
- 商户后台系统将code_url值生成二维码图片。
- 用户使用微信客户端扫码后发起支付。
注意:code_url有效期为2小时,过期后扫码不能再发起支付
附上微信官方序列图,个人觉得还是挺通俗易懂的~
相关实体
WeChatPayInfo (微信支付实体)
代码示例/**
* 微信支付信息实体类
*/
@Data
public class WeChatPayInfo {
// 应用ID
private String appid;
// 商户号
private String mch_id;
// 终端设备号(门店号或收银设备ID),默认请传"WEB"
private String device_info = "WEB";
// 随机字符串
private String nonce_str;
// 签名,信息填充完整后使用工具类设置签名
private String sign;
// 签名类型,目前支持HMAC-SHA256和MD5,默认为MD5
private String sign_type = "MD5";
/**
* 商品描述交易字段格式根据不同的应用场景按照以下格式: APP——需传入应用市场上的APP名字-实际商品名称
*/
private String body;
// 附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据
private String attach;
// 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一
private String out_trade_no;
// 符合ISO 4217标准的三位字母代码,默认人民币:CNY,其他值列表详见
private String fee_type = "CNY";
// 订单总金额,单位为分
private int total_fee;
//订单详情,用于单个商品的优惠,设置成对象的json
private String detail;
// 用户端实际ip
private String spbill_create_ip;
// 接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数。
private String notify_url;
// 交易类型
private String trade_type;
// 该字段用于统一下单时上报场景信息,目前支持上报实际门店信息,设置成对象的json
private String scene_info;
//微信公众号支付必填
private String openid;
//限制使用信用卡支付
private String limit_pay;
//二维码有效时间
private String time_expire;
/**
* 设置限制使用信用卡
*/
public void configureLimitPay() {
this.limit_pay = "no_credit";
}
/**
* 设置必填的自定义参数
*/
public WeChatPayInfo(String body, String out_trade_no, String suffix,
int total_fee, String trade_type, String spbill_create_ip) throws IOException {
this.body = WeChatPayConfigurations.getAppName() + "-" + body;
this.out_trade_no = out_trade_no;
this.notify_url = WeChatPayConfigurations.getNotifyUrl(suffix);
this.trade_type = trade_type;
this.spbill_create_ip = spbill_create_ip;
if (!WeChatPayConfigurations.getPayEnvironment()) {
this.total_fee = 1;
} else {
this.total_fee = total_fee;
}
}
/**
*构造函数1- 设置必填的自定义参数
*/
public WeChatPayInfo(String body, String out_trade_no,
int total_fee, String notify_url,
String trade_type, String spbill_create_ip) throws IOException {
this.body = WeChatPayConfigurations.getAppName() + "-" + body;
this.out_trade_no = out_trade_no;
this.notify_url = notify_url;
this.trade_type = trade_type;
this.spbill_create_ip = spbill_create_ip;
if (!WeChatPayConfigurations.getPayEnvironment()) {
this.total_fee = 1;
} else {
this.total_fee = total_fee;
}
}
/**
*构造函数2- 设置必填的自定义参数
*/
public WeChatPayInfo(String body, String out_trade_no, int total_fee, String notify_url,
String trade_type, String spbill_create_ip, String openid) {
this.body = body;
this.out_trade_no = out_trade_no;
if (!WeChatPayConfigurations.getPayEnvironment()) {
this.total_fee = 1;
} else {
this.total_fee = total_fee;
}
this.spbill_create_ip = spbill_create_ip;
this.notify_url = notify_url;
this.trade_type = trade_type;
this.openid = openid;
}
/**
*构造函数3- 设置必填的自定义参数
*/
public WeChatPayInfo(String body, String out_trade_no, int total_fee, String notify_url,
String trade_type, String spbill_create_ip, String openid,String appid,String mch_id) {
this.body = body;
this.out_trade_no = out_trade_no;
if (!WeChatPayConfigurations.getPayEnvironment()) {
this.total_fee = 1;
} else {
this.total_fee = total_fee;
}
this.spbill_create_ip = spbill_create_ip;
this.notify_url = notify_url;
this.trade_type = trade_type;
this.openid = openid;
this.appid = appid;
this.mch_id = mch_id;
}
/**
* 设置单品优惠信息
*/
public void configDetail(OrderInfos orderInfos) {
this.detail = JSON.toJSONString(orderInfos);
}
/**
* 设置实际门店信息
*/
public void configScene_info(SceneInfo sceneInfo) {
this.scene_info = JSON.toJSONString(sceneInfo);
}
}
WeChatPreOrderInfo (微信预支付实体)
代码示例
/**
* 微信预支付订单返回信息
*/
@Data
public class WeChatPreOrderInfo {
//返回状态码
private String return_code;
//返回信息
private String return_msg;
//应用APPID
private String appid;
//商户号
private String mch_id;
//设备号
private String device_info;
//随机字符串
private String nonce_str;
//签名
private String sign;
//业务结果
private String result_code;
//错误代码
private String err_code;
//错误代码描述
private String err_code_des;
//交易类型
private String trade_type;
//预支付交易会话标识
private String prepay_id;
//扫码支付返回字段,用于生成二维码
private String code_url;
//二维码有效时间
private String time_expire;
/**
* 连接是否成功
*/
public boolean isContact() {
return "SUCCESS".equals(this.return_code);
}
/**
* 业务是否成功
*/
public boolean isSuccess() {
if (isContact()) {
return "SUCCESS".equals(this.result_code);
}
return false;
}
/**
* 固定字段
*/
public String getPackage() {
return "Sign=WXPay";
}
/**
* 时间戳
*/
public Long getTimestamp() {
return System.currentTimeMillis() / 1000;
}
}
WeChatPayRet (微信支付返回结果实体)
代码示例
/**
* 微信支付返回信息类
*/
@Data
public class WeChatPayRet {
//返回状态码
private String return_code;
//返回信息
private String return_msg;
//应用ID
private String appid;
//商户号
private String mch_id;
//设备号
private String device_info;
//随机字符串
private String nonce_str;
//签名
private String sign;
//业务结果
private String result_code;
//错误代码
private String err_code;
//错误代码描述
private String err_des;
//用户标识
private String openid;
//是否关注公众账号
private String is_subscribe;
//交易类型
private String trade_type;
//付款银行
private String bank_type;
//总金额
private Integer total_fee;
//货币种类
private String fee_type;
//现金支付金额
private Integer cash_fee;
//现金支付货币类型
private String cash_fee_type;
//代金券金额
private Integer coupon_fee;
//代金券使用数量
private Integer coupon_count;
//微信支付订单号
private String transaction_id;
//商户订单号
private String out_trade_no;
//商家数据包
private String attach;
//加密方式
private String sign_type;
//支付完成时间
//支付完成时间
private String time_end;
/**
* 连接是否成功
*/
public boolean isContact() {
return "SUCCESS".equals(this.return_code);
}
/**
* 业务是否成功
*/
public boolean isSuccess() {
if (isContact()) {
return "SUCCESS".equals(this.result_code);
}
return false;
}
@Override
public String toString() {
return "WeChatPayRet{" +
"return_code='" + return_code + '\'' +
", return_msg='" + return_msg + '\'' +
", appid='" + appid + '\'' +
", mch_id='" + mch_id + '\'' +
", device_info='" + device_info + '\'' +
", nonce_str='" + nonce_str + '\'' +
", sign='" + sign + '\'' +
", result_code='" + result_code + '\'' +
", err_code='" + err_code + '\'' +
", err_des='" + err_des + '\'' +
", openid='" + openid + '\'' +
", is_subscribe='" + is_subscribe + '\'' +
", trade_type='" + trade_type + '\'' +
", bank_type='" + bank_type + '\'' +
", total_fee=" + total_fee +
", fee_type='" + fee_type + '\'' +
", cash_fee=" + cash_fee +
", cash_fee_type='" + cash_fee_type + '\'' +
", coupon_fee=" + coupon_fee +
", coupon_count=" + coupon_count +
", transaction_id='" + transaction_id + '\'' +
", out_trade_no='" + out_trade_no + '\'' +
", attach='" + attach + '\'' +
", sign_type='" + sign_type + '\'' +
", time_end='" + time_end + '\'' +
'}';
}
}
注意:随着微信版本的更新微信官方会扩展出新的字段,届时注意接收实体的局部变动
相关工具类
IpUtil(ip获取工具类)
代码示例
import org.apache.commons.lang.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* 获取用户侧ip地址
*/
public class IpUtil {
public static String getIp(HttpServletRequest request) {
String ip = request.getHeader("X-Real-IP");
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip.trim())) {
ip = request.getHeader("remote-host");
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip.trim())) {
ip = request.getRemoteAddr();
}
if (StringUtils.isNotBlank(ip)) {
if (ip.startsWith("10.")) {
String tip = request.getParameter("ip");
if (StringUtils.isNotBlank(tip)) {
ip = tip;
}
}
}
return ip;
}
}
SnGenerator(随机字符串生成器)
代码示例
/**
* 随机字符串生成工具
*/
public class SnGenerator {
private final static char[] NUMS = "123456789".toCharArray();
private final static char[] LETTERS = "QWERTYUIPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm"
.toCharArray();
private final static char[] MIX_LETTERS_AND_NUM = "QWERTYUIPASDFGHJKLZXCVBNMqwertyuipasdfghjklzxcvbnm01234567890"
.toCharArray();
public final static int MODE_NUM = 0;
public final static int MODE_LOWER_STR = 1;
public final static int MODE_UPPER_STR = 2;
public final static int MODE_STR = 3;
public final static int MODE_MIX = 4;
/**
* 生成带前缀的字符串,如果前缀+日期字符串+随机字符串的长度超过count,将会在保留前缀的情况下压缩其余部分
*/
public static String generateFormatWithPrefix(String prefix, int count, int mode) {
return generateFormat(prefix, null, count, mode);
}
/**
* 生成带前缀的字符串,如果日期字符串+随机字符串+后缀的长度超过count,将会在保留后缀的情况下压缩其余部分
*/
public static String generateFormatWithSuffix(String suffix, int count, int mode) {
return generateFormat(null, suffix, count, mode);
}
/**
* 生成不带前后缀的字符串,格式为yyyyMMddHHmmssSSS+随机字符串,长度为count
*/
public static String generateFormat(int count, int mode) {
return generateFormat(null, null, count, mode);
}
/**
* 生成格式化的字符串,格式为前缀+日期字符串(17位)+中间随机字符串+后缀。如果前缀+后缀+中间字符串的长度超过count, 将会压缩中间字符串的长度来满足count
*
* @param prefix 字符串前缀
* @param suffix 字符串后缀
* @param count 生成字符串的长度
* @param mode 模式
*/
public static String generateFormat(String prefix, String suffix, int count, int mode) {
if (count <= 17) {
count = 18;
}
int prefixLen = 0;
int suffixLen = 0;
StringBuilder sb = new StringBuilder();
if (prefix != null && (!"".equals(prefix))) {
prefixLen = prefix.length();
sb.append(prefix);
}
if (suffix != null && (!"".equals(suffix))) {
suffixLen = suffix.length();
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
String date = sdf.format(new Date());
int len = count - prefixLen - suffixLen - date.length();
if (len > 0) {
switch (mode) {
case MODE_NUM:
date = date + randomNums(len);
break;
case MODE_LOWER_STR:
date = date + randomLowerStr(len);
break;
case MODE_UPPER_STR:
date = date + randomUpperStr(len);
break;
case MODE_STR:
date = date + randomStr(len);
break;
case MODE_MIX:
date = date + randomMix(len);
break;
default:
date = date + randomNums(len);
break;
}
}
sb.append(date.substring(0, count - prefixLen - suffixLen));
if (suffixLen > 0) {
sb.append(suffix);
}
return sb.toString();
}
/**
* 生成写字母和数字随机字符串
*/
public static String randomMix(int count) {
return generator(count, MIX_LETTERS_AND_NUM);
}
/**
* 生成大小写混合字母随机字符串
*/
public static String randomStr(int count) {
return generator(count, LETTERS);
}
/**
* 生成纯大写字母随机字符串
*/
public static String randomUpperStr(int count) {
return generator(count, LETTERS).toUpperCase();
}
/**
* 生成纯数字随机字符串
*/
public static String randomNums(int count) {
return generator(count, NUMS);
}
/**
* 生成纯小写字母随机字符串
*/
public static String randomLowerStr(int count) {
return generator(count, LETTERS).toLowerCase();
}
/**
* 生成器
*/
private static String generator(int count, char[] arr) {
if (count <= 0) {
count = 6;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
double d = Math.random();
int index = (int) Math.floor(d * arr.length);
sb.append(arr[index]);
}
return sb.toString();
}
}
XMLUtils(xml转化工具类)
代码示例
/**
* xml转化工具类
*/
public class XMLUtils {
/**
* 从request读取xml
*/
public static String readXmlFromRequest(HttpServletRequest request) {
StringBuilder xmlSb = new StringBuilder();
try (
ServletInputStream in = request.getInputStream();
InputStreamReader inputStream = new InputStreamReader(in);
BufferedReader buffer = new BufferedReader(inputStream);
) {
String line;
while ((line = buffer.readLine()) != null) {
xmlSb.append(line);
}
} catch (Exception e) {
e.printStackTrace();
}
return xmlSb.toString();
}
/**
* 将获取的Map转换成xml
*/
public static Document convertMap2Xml(Map<String, Object> map) {
Document doc = DocumentHelper.createDocument();
try {
Element root = doc.addElement("xml");
Set<String> keys = map.keySet();
for (String key : keys) {
Element ele = root.addElement(key);
ele.addCDATA(map.get(key).toString());
}
} catch (Exception e) {
e.printStackTrace();
}
return doc;
}
/**
* xml文档Document转对象
*/
@SuppressWarnings("unchecked")
public static Object convertXml2Bean(Document document, Class<?> clazz) {
Map<String, String> map = new HashMap<>();
// 获取根节点
Element root = document.getRootElement();
try {
List<Element> properties = root.elements();
for (Element pro : properties) {
String propName = pro.getName();
String propValue = pro.getText();
map.put(propName, propValue);
}
} catch (Exception e) {
e.printStackTrace();
}
//处理map里的JSON字符串字段,防止解析错误
Map<String, Object> objMap = new TreeMap<>();
Set<String> keys = map.keySet();
for (String key : keys) {
String str = map.get(key);
try {
//如果是JSON字符串,则转换成对象,再添加到objMap中
objMap.put(key, JSON.parse(str));
} catch (JSONException e) {
//如果不是JSON字符串,则直接添加到objMap中
objMap.put(key, str);
} catch (Exception e) {
//其余错误抛出
e.printStackTrace();
}
}
return JSON.parseObject(JSON.toJSONString(map), clazz);
}
/**
* xml字符串转对象
*/
public static Object convertXml2Bean(String xmlString, Class<?> clazz) {
Document document;
try {
document = DocumentHelper.parseText(xmlString);
} catch (DocumentException e) {
throw new RuntimeException("获取Document异常" + xmlString);
} // 获取根节点
return convertXml2Bean(document, clazz);
}
/**
* 对象转xml文件
*/
public static Document convertBean2Xml(Object b) {
Document document = DocumentHelper.createDocument();
try {
// 创建根节点元素
Element root = document.addElement(b.getClass().getSimpleName());
// 获取实体类b的所有属性,返回Field数组
Field[] field = b.getClass().getDeclaredFields();
// 遍历所有有属性
for (Field aField : field) {
String name = aField.getName(); // 获取属属性的名字
if (!name.equals("serialVersionUID")) {// 去除串行化序列属性
name = name.substring(0, 1).toUpperCase() + name.substring(1); // 将属性的首字符大写,方便构造get,set方法
Method m = b.getClass().getMethod("get" + name);
String propValue = (String) m.invoke(b);// 获取属性值
Element propertie = root.addElement(name);
propertie.setText(propValue);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return document;
}
/**
* 对象转xml格式的字符串
*/
public static String getXmlString(Object b) {
return convertBean2Xml(b).asXML();
}
}
WeChatPayAssistant (微信支付助手)
代码示例
public class WeChatPayAssistant {
private static final Logger logger = LoggerFactory.getLogger(WeChatPayAssistant.class);
/**
* 解析付款回调请求
*/
public static WeChatPayRet parsePayNotifyRequest(HttpServletRequest request) {
String xml = XMLUtils.readXmlFromRequest(request);
logger.info("wechatXml is: "+xml);
return (WeChatPayRet) XMLUtils.convertXml2Bean(xml, WeChatPayRet.class);
}
/**
* 解析退款回调请求
*/
public static WeChatRefundNotifyRet parseRefundNotifyRequest(HttpServletRequest request) {
String xml = XMLUtils.readXmlFromRequest(request);
return (WeChatRefundNotifyRet) XMLUtils.convertXml2Bean(xml, WeChatRefundNotifyRet.class);
}
/**
* 应答微信回调
*/
public static void echo(HttpServletResponse response) throws Exception {
response.setContentType("application/xml");
ServletOutputStream os = response.getOutputStream();
os.print(echo());
}
/**
* 异步回调应答
*/
public static String echo() {
return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
}
/**
* 微信公众号或移动支付退款
*/
public static WeChatRefundRet refund(WeChatRefundInfo refundInfo, String tradeType)
throws Exception {
refundInfo.setNotify_url(WeChatPayConfigurations.getNotifyUrl("refund"));
refundInfo.setNonce_str(SnGenerator.randomMix(32));
String xml = "";
refundInfo.setAppid(WeChatPayConfigurations.getAppId());
refundInfo.setMch_id(WeChatPayConfigurations.getMchId());
refundInfo.setSign(generateSign(refundInfo));
TreeMap<String, Object> map = getSignMap(refundInfo, WeChatRefundInfo.class);
Document doc = XMLUtils.convertMap2Xml(map);
URI uri = new URIBuilder().setScheme("https").setHost("api.mch.weixin.qq.com")
.setPath("/secapi/pay/refund")
.build();
xml = HttpClientUtils.connectWithXMLAndSSLByPost(uri, doc,
WeChatPayConfigurations.getRefundCertificatePath(),
WeChatPayConfigurations.getRefundCertificatePassword());
WeChatRefundRet refundRet = (WeChatRefundRet) XMLUtils
.convertXml2Bean(xml, WeChatRefundRet.class);
if (!refundRet.isContact()) {
String msg = refundRet.getReturn_code() + ":" + refundRet.getReturn_msg();
msg = new String(msg.getBytes("iso-8859-1"), "utf-8");
throw new RuntimeException(msg);
}
if (!refundRet.isSuccess()) {
String msg = refundRet.getResult_code() + ":" + refundRet.getErr_code() + " - " + refundRet
.getErr_code_des();
throw new RuntimeException(msg);
}
return refundRet;
}
/**
* 微信预支付订单
*/
public static WeChatPreOrderInfo preOrder(WeChatPayInfo payInfo) throws Exception {
if (payInfo.getTrade_type().equals(WeChatPayConst.TRADE_TYPE_OFFICIAL_ACCOUNT) && StringUtils
.isEmpty(payInfo.getOpenid())) {
throw new RuntimeException("公众号支付openid不能为空,请填入正确的openid");
}
payInfo.setAppid(WeChatPayConfigurations.getAppId());
payInfo.setMch_id(WeChatPayConfigurations.getMchId());
payInfo.setNonce_str(SnGenerator.randomMix(32).toUpperCase());
payInfo.setTime_expire(getOrderExpireTime(2*60*60*1000L));
payInfo.setSign(generateSign(payInfo));
Document doc = XMLUtils.convertMap2Xml(getSignMap(payInfo, WeChatPayInfo.class));
URI uri = new URIBuilder().setScheme("https").setHost("api.mch.weixin.qq.com")
.setPath("/pay/unifiedorder")
.build();
String xml = HttpClientUtils.connectWithXMLByPost(uri, doc);
WeChatPreOrderInfo info = (WeChatPreOrderInfo) XMLUtils
.convertXml2Bean(xml, WeChatPreOrderInfo.class);
if (!info.isContact()) {
String msg = info.getReturn_code() + ":" + info.getReturn_msg();
msg = new String(msg.getBytes("iso-8859-1"), "utf-8");
throw new RuntimeException(msg);
}
if (!info.isSuccess()) {
String msg = info.getResult_code() + ":" + WeChatPayErrorUtil.getErrorMsg(info.getErr_code());
throw new RuntimeException(msg);
}
return info;
}
/**
* 设置微信二维码失效时间,并返回具体失效的时间点
* expire 二维码的有效时间,单位是毫秒
*/
public static String getOrderExpireTime(Long expire){
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
Date now = new Date();
Date afterDate = new Date(now.getTime() + expire);
return sdf.format(afterDate );
}
/**
* 设置签名
*/
public static String generateSign(Object obj) throws Exception {
TreeMap<String, Object> map = getSignMap(obj, obj.getClass());
return signMap(map);
}
/**
* 验证签名
*/
public static boolean isSignValid(Object obj) throws Exception {
TreeMap<String, Object> map = getSignMap(obj, obj.getClass());
String signFromAPIResponse = map.get("sign").toString();
if (signFromAPIResponse == null || signFromAPIResponse.equals("")) {
logger.warn("The data signature data returned by the API does not exist and may be tampered with by a third party!!!");
return false;
}
logger.info("服务器回包里面的签名是: {}", signFromAPIResponse);
//清掉返回数据对象里面的Sign数据(不能把这个数据也加进去进行签名),然后用签名算法进行签名
map.remove("sign");
logger.info("sign map: {}", new JSONObject(map));
String signForAPIResponse = signMap(map);
if (!signForAPIResponse.equals(signFromAPIResponse)) {
//签名验不过,表示这个API返回的数据有可能已经被篡改了
logger.warn("The data signature verification returned by the API fails, and may be tampered with by a third party!!! signForAPIResponse The resulting signature is" + signForAPIResponse);
return false;
}
return true;
}
/**
* TreeMap加签
*/
private static String signMap(TreeMap<String, Object> map) {
Set<String> keys = map.keySet();
StringBuilder sb = new StringBuilder();
for (String key : keys) {
sb.append(key).append("=").append(map.get(key)).append("&");
}
sb.append("key").append("=").append(WeChatPayConfigurations.getPayKey());
return DigestUtils.md5Hex(sb.toString()).toUpperCase();
}
/**
* 获取按顺序整理好的非空值字段
*/
@SuppressWarnings("unchecked")
private static TreeMap<String, Object> getSignMap(Object obj, Class<?> clz) throws Exception {
if (obj == null) {
throw new RuntimeException("支付对象不能为空");
}
// 使用treeMap将字段按要求排序
TreeMap<String, Object> map = new TreeMap<>();
if (obj instanceof Map){
Map mapObj = (Map)obj;
Set<Map.Entry<Object,Object>> set = mapObj.entrySet();
for (Map.Entry<Object,Object> entry:set){
map.put(entry.getKey().toString(),entry.getValue());
}
}else {
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
map.put(field.getName(), clz.getMethod("get" + upperFirst(field.getName())).invoke(obj));
}
}
// 使用fastjson过滤掉null的字段
String json = JSON.toJSONString(map);
map = JSON.parseObject(json, TreeMap.class);
return map;
}
/**
* 首字母大写
*/
private static String upperFirst(String name) {
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
/**
* APP支付二次加签
*/
public static Map<String, Object> sign4App(WeChatPreOrderInfo preOrderInfo) {
TreeMap<String, Object> map = new TreeMap<>();
map.put("appid", preOrderInfo.getAppid());
map.put("partnerid", preOrderInfo.getMch_id());
map.put("prepayid", preOrderInfo.getPrepay_id());
map.put("package", preOrderInfo.getPackage());
map.put("noncestr", SnGenerator.randomMix(32));
map.put("timestamp", preOrderInfo.getTimestamp());
map.put("sign", signMap(map));
return map;
}
/**
* 微信内H5支付二次加签(注意:APP加签字段全部小写,这里加签用驼峰)
*/
public static Map<String, Object> sign4WxH5(WeChatPreOrderInfo preOrderInfo) {
TreeMap<String, Object> map = new TreeMap<>();
map.put("appId", preOrderInfo.getAppid());
map.put("timeStamp", preOrderInfo.getTimestamp().toString());
map.put("package", "prepay_id=" + preOrderInfo.getPrepay_id());
map.put("nonceStr", SnGenerator.randomMix(32));
map.put("signType", "MD5");
map.put("paySign", signMap(map));
return map;
}
public static WeChatRefundReqInfo decodeReqInfo(String reqInfo) throws Exception {
String md5Key = DigestUtils.md5Hex(WeChatPayConfigurations.getPayKey());
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
SecretKey keySpec = new SecretKeySpec(md5Key.getBytes(), "AES"); //生成加密解密需要的Key
cipher.init(Cipher.DECRYPT_MODE, keySpec);
String result = new String(cipher.doFinal(Base64.decodeBase64(reqInfo)),
StandardCharsets.UTF_8);
return (WeChatRefundReqInfo) XMLUtils.convertXml2Bean(result, WeChatRefundReqInfo.class);
}
}
微信支付常量
代码示例
/**
* 微信支付常量词典
*/
public class WeChatPayConst {
// 签名类型md5
public static final String SIGN_TYPE_MD5 = "MD5";
// 签名类型sha256
public static final String SIGN_TYPE_SHA256 = "HMAC-SHA256";
// 支付类型:公众号支付
public static final String TRADE_TYPE_OFFICIAL_ACCOUNT = "JSAPI";
// 支付类型:扫码支付
public static final String TRADE_TYPE_SWEEP_CODE = "NATIVE";
// 支付类型:APP
public static final String TRADE_TYPE_APP = "APP";
}
关键步骤
-
支付账号的开通
参考微信官方文档 -
微信预支付
-
确定金额,生成商户系统相关订单信息,初始化状态为待支付
-
构造微信支付实体(WeChatPayInfo)
WeChatPayInfo weChatPayInfo = new WeChatPayInfo(BODY, orderId, amount, notifyUrl, WeChatPayConst.TRADE_TYPE_SWEEP_CODE, ip);
-
获取用户侧ip
-
设置签名
-
生成随机字符串
-
定义回调接口url(notifyUrl)
-
按要求将微信支付实体转化为xml
-
调用微信统一下单接口
Document doc = XMLUtils.convertMap2Xml(getSignMap(payInfo, WeChatPayInfo.class)); URI uri = new URIBuilder().setScheme("https").setHost("api.mch.weixin.qq.com").setPath("/pay/unifiedorder").build();
- 将返回结果转化为微信预支付实体类(WeChatPreOrderInfo),并获取二维码url
-
备注:预支付详细代码见WeChatPayAssistant工具类的
preOrder(WeChatPayInfo payInfo)方法
- 微信回调(微信支付结果通知)
- 判断连接和业务是否都成功
- 验签
- 修改订单状态(支付成功或失败)
- 返回回调应答
public String weChatPayNotify(HttpServletRequest request) {
WeChatPayRet weChatPayRet = WeChatPayAssistant.parsePayNotifyRequest(request);
//判断连接和业务是否都成功且通过验签
if (weChatPayRet.isContact() && weChatPayRet.isSuccess() && WeChatPayAssistant.isSignValid(weChatPayRet)) {
//修改订单状态及相关业务处理
}
//返回应答
return WeChatPayAssistant.echo();
}
4.如果未收到微信支付回调通知,可主动调用微信查询订单状态接口,
此处逻辑省略 微信官方查询订单
小程序支付
小程序与扫码支付的主要不同之处即小程序需要先调起微信登录接口( wx.login(Object object) )获取登录凭证(code),然后通过code进而换取用户登录态信息,包括用户的唯一标识(openid)及本次登录的会话密钥(session_key)等。
小程序支付序列图:
关键步骤
-
根据code获取openId并将session_key存入redis
public static String getOpenId(String appId,String appSecret,String code){ String s = HttpClientUtil.sendHttpGet(getOpenIdUrl + "&appid=" + appId + "&secret=" + appSecret+"&js_code="+code); if (null!=s&&!"".equals(s)){ JSONObject jsonObject = JSON.parseObject(s); String openId = jsonObject.getString("openid"); //把session_key存进redis String session_key = jsonObject.getString("session_key"); JedisCluster jedisCluster = RedisUtil.getJedisCluster(); String s1 = jedisCluster.get(RedisUtil.getKey(sessionKeyPre + openId)); if (null!=openId&&!"".equals(openId)){ if (null==s1||"".equals(s1)||!s1.equals(session_key)){ jedisCluster.set(RedisUtil.getKey(sessionKeyPre+openId),session_key); jedisCluster.expire(RedisUtil.getKey(sessionKeyPre+openId),86400*2); } return openId; } else { Integer errcode = jsonObject.getInteger("errcode"); String errmsg = jsonObject.getString("errmsg"); log.error("getOpenId is failed errcode:"+errcode+"-----errmsg:"+errmsg); throw new RuntimeException("获取openId失败"); } }else { log.error("getOpenId is failed "); throw new RuntimeException("获取openId失败"); } }
-
微信预支付(同扫码支付,但不返回用于生成二维码的code_url)需将预支付交易会话标识prepay_id以及拼接好的数据包package返给前端
-
微信回调(同扫码支付)