完成微信转账以及demo实例

This commit is contained in:
b2baccline
2021-02-25 16:33:35 +08:00
parent 15396ba3cf
commit cff403e037
22 changed files with 1304 additions and 6 deletions

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ballcat-extends</artifactId>
<groupId>com.hccake</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ballcat-extend-pay-wx</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.hccake</groupId>
<artifactId>ballcat-common-core</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,229 @@
package com.hccake.starte.pay.wx;
import static com.hccake.starte.pay.wx.constants.WxPayConstant.HUNDRED;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.hccake.starte.pay.wx.constants.WxPayConstant;
import com.hccake.starte.pay.wx.domain.DefaultWxDomain;
import com.hccake.starte.pay.wx.domain.WxDomain;
import com.hccake.starte.pay.wx.enums.RequestSuffix;
import com.hccake.starte.pay.wx.enums.SignType;
import com.hccake.starte.pay.wx.enums.TradeType;
import com.hccake.starte.pay.wx.response.WxPayCallback;
import com.hccake.starte.pay.wx.response.WxPayOrderQueryResponse;
import com.hccake.starte.pay.wx.response.WxPayResponse;
import com.hccake.starte.pay.wx.utils.WxPayUtil;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;
import lombok.Data;
import lombok.SneakyThrows;
/**
* @author lingting 2021/1/26 15:54
*/
@Data
public class WxPay {
private final String appId;
private final String mchId;
private final String mckKey;
private String notifyUrl;
private String returnUrl;
/**
* 是否使用沙箱
*/
private boolean sandbox;
/**
* 域名策略, 可是使用自定义实现
*/
private WxDomain domain;
public WxPay(String appId, String mchId, String mckKey, boolean sandbox) {
this(appId, mchId, mckKey, sandbox, DefaultWxDomain.of(sandbox));
}
public WxPay(String appId, String mchId, String mckKey, boolean sandbox, WxDomain domain) {
this.sandbox = sandbox;
this.domain = domain;
this.mchId = mchId;
this.appId = appId;
// 沙箱环境初始化
if (sandbox) {
this.mckKey = domain.sandbox(this).getSandboxSignKey();
}
else {
this.mckKey = mckKey;
}
}
/**
* jsApi 支付
* @param sn 订单号
* @param amount 支付金额, 单位 元
* @param ip 客户端ip
* @param body 商品描述
*/
public WxPayResponse jsApiPay(String sn, BigDecimal amount, String ip, String body) {
return jsApiPay(sn, amount, ip, body, notifyUrl);
}
public WxPayResponse jsApiPay(String sn, BigDecimal amount, String ip, String body, String notifyUrl) {
return WxPayResponse.of(pay(sn, amount, ip, body, notifyUrl, TradeType.JSAPI));
}
/**
* app 支付
* @param sn 订单号
* @param amount 支付金额, 单位 元
* @param ip 客户端ip
* @param body 商品描述
*/
public WxPayResponse appPay(String sn, BigDecimal amount, String ip, String body) {
return appPay(sn, amount, ip, body, notifyUrl);
}
public WxPayResponse appPay(String sn, BigDecimal amount, String ip, String body, String notifyUrl) {
return WxPayResponse.of(pay(sn, amount, ip, body, notifyUrl, TradeType.APP));
}
/**
* native 支付
* @param sn 订单号
* @param amount 支付金额, 单位 元
* @param ip 客户端ip
* @param body 商品描述
*/
public WxPayResponse nativePay(String sn, BigDecimal amount, String ip, String body) {
return nativePay(sn, amount, ip, body, notifyUrl);
}
public WxPayResponse nativePay(String sn, BigDecimal amount, String ip, String body, String notifyUrl) {
return WxPayResponse.of(pay(sn, amount, ip, body, notifyUrl, TradeType.NATIVE));
}
/**
* web 支付
* @param sn 订单号
* @param amount 支付金额, 单位 元
* @param ip 客户端ip
* @param body 商品描述
*/
public WxPayResponse webPay(String sn, BigDecimal amount, String ip, String body) {
return webPay(sn, amount, ip, body, notifyUrl);
}
public WxPayResponse webPay(String sn, BigDecimal amount, String ip, String body, String notifyUrl) {
return WxPayResponse.of(pay(sn, amount, ip, body, notifyUrl, TradeType.MWEB));
}
/**
* 发起支付
* @param sn 订单号
* @param amount 金额, 单位 元
* @param ip ip
* @param body 描述
* @param notifyUrl 通知
* @param tradeType 支付类型
* @author lingting 2021-02-25 10:19
*/
public Map<String, String> pay(String sn, BigDecimal amount, String ip, String body, String notifyUrl,
TradeType tradeType) {
Map<String, String> params = new HashMap<>(6);
params.put("body", body);
params.put("out_trade_no", sn);
params.put("total_fee", yuanToFen(amount));
params.put("spbill_create_ip", ip);
params.put("notify_url", notifyUrl);
params.put("trade_type", tradeType.toString());
return request(params, RequestSuffix.UNIFIEDORDER);
}
/**
* 查询订单
* @param sn 平台订单号
* @param wxSn 微信订单号
* @return com.hccake.starte.pay.wx.response.WxPayOrderQueryResponse
* @author lingting 2021-02-25 15:20
*/
public WxPayOrderQueryResponse query(String sn, String wxSn) {
Assert.isFalse(StrUtil.isBlank(sn) && StrUtil.isBlank(wxSn), "参数 sn 和 wxSn 不能同时为空!");
Map<String, String> params = new HashMap<>(6);
params.put("out_trade_no", sn);
params.put("transaction_id", wxSn);
return WxPayOrderQueryResponse.of(request(params, RequestSuffix.ORDERQUERY));
}
/**
* 向微信发起请求
* @param params 参数
* @param rs 请求后缀
* @author lingting 2021-01-29 18:12
*/
@SneakyThrows
public Map<String, String> request(Map<String, String> params, RequestSuffix rs) {
Map<String, String> map = new HashMap<>(params.size() + 3);
map.putAll(params);
// 添加必须参数
map.put("appid", appId);
map.put("mch_id", mchId);
map.put("nonce_str", WxPayUtil.generateNonceStr());
// 设置签名类型; 沙箱使用 md5, 正式使用 hmac sha256
map.put(WxPayConstant.FIELD_SIGN_TYPE, sandbox ? SignType.MD5.getStr() : SignType.HMAC_SHA256.getStr());
// 签名
map.put(WxPayConstant.FIELD_SIGN, WxPayUtil.sign(map, mckKey));
return domain.request(map, rs);
}
/**
* 金额单位转换, 元 转为 分
* @param amount 支付金额, 单位 元
* @return java.lang.String
* @author lingting 2021-01-25 10:27
*/
public String yuanToFen(BigDecimal amount) {
return amount.multiply(HUNDRED).setScale(2, RoundingMode.UP).toBigInteger().toString();
}
/**
* 验证回调签名
* @param callback 回调数据
* @return java.lang.Boolean
* @author lingting 2021-02-25 16:01
*/
public boolean checkSign(WxPayCallback callback) {
Map<String, String> params = new HashMap<>(callback.getRaw().size());
params.putAll(callback.getRaw());
// 存在签名类型, 直接验签
if (params.containsKey(WxPayConstant.FIELD_SIGN_TYPE)) {
return WxPayUtil.sign(params, mckKey).equals(callback.getSign());
}
// 两种签名类型都试一次
params.put(WxPayConstant.FIELD_SIGN_TYPE, SignType.HMAC_SHA256.getStr());
if (WxPayUtil.sign(params, mckKey).equals(callback.getSign())) {
return true;
}
params.put(WxPayConstant.FIELD_SIGN_TYPE, SignType.MD5.getStr());
if (WxPayUtil.sign(params, mckKey).equals(callback.getSign())) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,36 @@
package com.hccake.starte.pay.wx.constants;
import java.math.BigDecimal;
/**
* @author lingting 2021/1/26 16:15
*/
public class WxPayConstant {
/**
* 一百
*/
public static final BigDecimal HUNDRED = new BigDecimal("100");
/**
* 签名字段名
*/
public static final String FIELD_SIGN = "sign";
/**
* 签名类型字段名
*/
public static final String FIELD_SIGN_TYPE = "sign_type";
/**
* 回调成功返回值
*/
public static String CALLBACK_SUCCESS = "<xml>\n" + " <return_code><![CDATA[SUCCESS]]></return_code>\n"
+ " <return_msg><![CDATA[OK]]></return_msg>\n" + "</xml>";
/**
* 回调验签失败返回值
*/
public static String CALLBACK_SIGN_ERROR = "<xml>\n" + " <return_code><![CDATA[FAIL]]></return_code>\n"
+ " <return_msg><![CDATA[签名异常]]></return_msg>\n" + "</xml>";
}

View File

@@ -0,0 +1,60 @@
package com.hccake.starte.pay.wx.domain;
import cn.hutool.http.HttpRequest;
import com.hccake.starte.pay.wx.enums.RequestSuffix;
import com.hccake.starte.pay.wx.utils.WxPayUtil;
import java.util.Map;
import lombok.SneakyThrows;
/**
* 微信域名管理
*
* @author lingting 2021/1/26 16:05
*/
public class DefaultWxDomain implements WxDomain {
private static final String FLAG = "/";
/**
* 是否使用沙箱
*/
private final boolean sandbox;
private DefaultWxDomain(boolean sandbox) {
this.sandbox = sandbox;
}
public static DefaultWxDomain of(boolean sandbox) {
return new DefaultWxDomain(sandbox);
}
@Override
@SneakyThrows
public String sendRequest(Map<String, String> params, RequestSuffix rs) {
// 获取请求地址
String url = getUrl(rs.getSuffix());
HttpRequest post = HttpRequest.post(url).header("Content-Type", "text/xml").body(WxPayUtil.mapToXml(params));
return post.execute().body();
}
/**
* 根据微信的建议, 这里后续需要加上主备切换的功能
* @return java.lang.String
* @author lingting 2021-01-29 17:50
*/
public String getDomain() {
return MAIN1;
}
public String getUrl(String suffix) {
if (suffix.startsWith(FLAG)) {
suffix = suffix.substring(1);
}
if (sandbox) {
return getDomain() + "sandboxnew/pay/" + suffix;
}
return getDomain() + "pay/" + suffix;
}
}

View File

@@ -0,0 +1,83 @@
package com.hccake.starte.pay.wx.domain;
import com.hccake.starte.pay.wx.WxPay;
import com.hccake.starte.pay.wx.constants.WxPayConstant;
import com.hccake.starte.pay.wx.enums.RequestSuffix;
import com.hccake.starte.pay.wx.enums.SignType;
import com.hccake.starte.pay.wx.response.WxPayResponse;
import com.hccake.starte.pay.wx.utils.WxPayUtil;
import java.util.HashMap;
import java.util.Map;
import lombok.SneakyThrows;
import org.slf4j.LoggerFactory;
/**
* @author lingting 2021/2/1 10:57
*/
public interface WxDomain {
/**
* 主域名
*/
String MAIN1 = "https://api.mch.weixin.qq.com/";
String MAIN2 = "https://api.weixin.qq.com/";
/**
* 备用域名
*/
String BACKUP1 = "https://api2.mch.weixin.qq.com/";
String BACKUP2 = "https://api2.weixin.qq.com/";
/**
* 发起请求. 根据微信建议,实现类最好拥有主备域名自动切换的功能
* @param params 参数
* @param rs 请求后缀
* @return java.util.Map<java.lang.String,java.lang.String>
* @author lingting 2021-02-01 10:58
*/
@SneakyThrows
default Map<String, String> request(Map<String, String> params, RequestSuffix rs) {
String res = "";
try {
res = sendRequest(params, rs);
return WxPayUtil.xmlToMap(res);
}
catch (Exception e) {
// 用于处理返回值异常情况
LoggerFactory.getLogger(getClass()).error("微信支付请求失败!返回值:\n {}", res);
throw e;
}
}
/**
* 发送请求
* @param params 参数
* @param rs 前缀
* @return java.lang.String
* @author lingting 2021-02-25 14:09
*/
@SneakyThrows
String sendRequest(Map<String, String> params, RequestSuffix rs);
/**
*
* 获取沙箱环境密钥
* @param wxPay 支付信息
* @return com.hccake.starte.pay.wx.response.WxPayResponse
* @author lingting 2021-02-25 14:49
*/
default WxPayResponse sandbox(WxPay wxPay) {
HashMap<String, String> map = new HashMap<>();
map.put("mch_id", wxPay.getMchId());
map.put("nonce_str", WxPayUtil.generateNonceStr());
// 设置签名类型
map.put(WxPayConstant.FIELD_SIGN_TYPE, SignType.MD5.getStr());
// 签名
map.put(WxPayConstant.FIELD_SIGN, WxPayUtil.sign(map, wxPay.getMckKey()));
return WxPayResponse.of(request(map, RequestSuffix.GETSIGNKEY));
}
}

View File

@@ -0,0 +1,34 @@
package com.hccake.starte.pay.wx.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 请求后缀
* @author lingting 2021/1/29 18:01
*/
@Getter
@AllArgsConstructor
public enum RequestSuffix {
/**
* 下单请求后缀
*/
UNIFIEDORDER("unifiedorder"),
/**
* 获取沙箱密钥
*/
GETSIGNKEY("getsignkey"),
/**
* 查询订单
*/
ORDERQUERY("orderquery"),
;
/**
* 后缀
*/
private final String suffix;
}

View File

@@ -0,0 +1,42 @@
package com.hccake.starte.pay.wx.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 返回code
* @author lingting 2021/2/1 11:31
*/
@Getter
@AllArgsConstructor
public enum ResponseCode {
/**
* 成功
*/
SUCCESS,
/**
* 失败
*/
FAIL,
/**
* 异常
*/
ERROR,
;
@JsonCreator
public static ResponseCode of(String status) {
switch (status) {
case "SUCCESS":
return SUCCESS;
case "FAIL":
return FAIL;
default:
return ERROR;
}
}
}

View File

@@ -0,0 +1,34 @@
package com.hccake.starte.pay.wx.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author lingting 2021/1/29 18:14
*/
@Getter
@AllArgsConstructor
public enum SignType {
/**
* 一般用于沙箱环境
*/
MD5("MD5"),
/**
* 一般用于正式环境
*/
HMAC_SHA256("HMAC-SHA256"),
;
private final String str;
public static SignType of(String str){
for (SignType e: values()){
if (e.str.equals(str)){
return e;
}
}
return null;
}
}

View File

@@ -0,0 +1,73 @@
package com.hccake.starte.pay.wx.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
/**
* @author lingting 2021/2/25 15:35
*/
public enum TradeState {
/**
* 支付成功
*/
SUCCESS,
/**
* 转入退款
*/
REFUND,
/**
* 未支付
*/
NOTPAY,
/**
* 已关闭
*/
CLOSED,
/**
* 已撤销(刷卡支付)
*/
REVOKED,
/**
* 用户支付中
*/
USERPAYING,
/**
* 支付失败(其他原因,如银行返回失败)
*/
PAYERROR,
/**
* 已接收,等待扣款
*/
ACCEPT,
/**
* 异常
*/
ERROR,
;
@JsonCreator
public static TradeState of(String status) {
switch (status) {
case "SUCCESS":
return SUCCESS;
case "REFUND":
return REFUND;
case "NOTPAY":
return NOTPAY;
case "CLOSED":
return CLOSED;
case "REVOKED":
return REVOKED;
case "USERPAYING":
return USERPAYING;
case "PAYERROR":
return PAYERROR;
case "ACCEPT":
return ACCEPT;
default:
return ERROR;
}
}
}

View File

@@ -0,0 +1,31 @@
package com.hccake.starte.pay.wx.enums;
/**
* 微信支付类型
* @author lingting 2021/2/25 10:15
*/
public enum TradeType {
/**
* 小程序或公众号
*/
JSAPI,
/**
* APP
*/
APP,
/**
* 原生
*/
NATIVE,
/**
* h5
*/
MWEB,
;
}

View File

@@ -0,0 +1,89 @@
package com.hccake.starte.pay.wx.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.hccake.ballcat.common.core.util.JacksonUtils;
import com.hccake.starte.pay.wx.WxPay;
import com.hccake.starte.pay.wx.enums.ResponseCode;
import com.hccake.starte.pay.wx.enums.TradeType;
import java.math.BigInteger;
import java.util.Map;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author lingting 2021/2/25 15:43
*/
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class WxPayCallback {
@JsonProperty("transaction_id")
private String transactionId;
@JsonProperty("nonce_str")
private String nonceStr;
@JsonProperty("bank_type")
private String bankType;
@JsonProperty("openid")
private String openid;
@JsonProperty("sign")
private String sign;
@JsonProperty("fee_type")
private String feeType;
@JsonProperty("mch_id")
private String mchId;
@JsonProperty("cash_fee")
private BigInteger cashFee;
@JsonProperty("out_trade_no")
private String outTradeNo;
@JsonProperty("appid")
private String appid;
@JsonProperty("total_fee")
private BigInteger totalFee;
@JsonProperty("trade_type")
private TradeType tradeType;
@JsonProperty("result_code")
private ResponseCode resultCode;
@JsonProperty("time_end")
private String timeEnd;
@JsonProperty("is_subscribe")
private String isSubscribe;
@JsonProperty("return_code")
private ResponseCode returnCode;
public static WxPayCallback of(Map<String, String> res) {
return JacksonUtils.toObj(JacksonUtils.toJson(res), WxPayCallback.class).setRaw(res);
}
/**
* 返回的原始数据
*/
private Map<String, String> raw;
/**
* 验签
* @param wxPay 微信支付信息
* @return boolean
* @author lingting 2021-02-25 16:04
*/
public boolean checkSign(WxPay wxPay) {
return wxPay.checkSign(this);
}
}

View File

@@ -0,0 +1,108 @@
package com.hccake.starte.pay.wx.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.hccake.ballcat.common.core.util.JacksonUtils;
import com.hccake.starte.pay.wx.enums.ResponseCode;
import com.hccake.starte.pay.wx.enums.TradeState;
import com.hccake.starte.pay.wx.enums.TradeType;
import java.math.BigInteger;
import java.util.Map;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author lingting 2021/2/25 15:19
*/
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class WxPayOrderQueryResponse {
@JsonProperty("transaction_id")
private String transactionId;
@JsonProperty("nonce_str")
private String nonceStr;
@JsonProperty("trade_state")
private TradeState tradeState;
@JsonProperty("bank_type")
private String bankType;
@JsonProperty("openid")
private String openid;
@JsonProperty("sign")
private String sign;
@JsonProperty("return_msg")
private String returnMsg;
@JsonProperty("fee_type")
private String feeType;
@JsonProperty("mch_id")
private String mchId;
@JsonProperty("cash_fee")
private BigInteger cashFee;
@JsonProperty("out_trade_no")
private String outTradeNo;
@JsonProperty("cash_fee_type")
private String cashFeeType;
@JsonProperty("appid")
private String appid;
@JsonProperty("total_fee")
private BigInteger totalFee;
@JsonProperty("trade_state_desc")
private String tradeStateDesc;
@JsonProperty("trade_type")
private TradeType tradeType;
@JsonProperty("result_code")
private ResponseCode resultCode;
@JsonProperty("attach")
private String attach;
@JsonProperty("time_end")
private String timeEnd;
@JsonProperty("is_subscribe")
private String isSubscribe;
@JsonProperty("return_code")
private ResponseCode returnCode;
public static WxPayOrderQueryResponse of(Map<String, String> res) {
return JacksonUtils.toObj(JacksonUtils.toJson(res), WxPayOrderQueryResponse.class).setRaw(res);
}
/**
* 返回的原始数据
*/
private Map<String, String> raw;
/**
* 交易是否成功 . 返回false 表示交易失败
* @return boolean
* @author lingting 2021-02-25 15:35
*/
public boolean isSuccess() {
// 交易成功
if (returnCode == ResponseCode.SUCCESS && resultCode == ResponseCode.SUCCESS
&& tradeState == TradeState.SUCCESS) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,123 @@
package com.hccake.starte.pay.wx.response;
import static com.hccake.starte.pay.wx.enums.ResponseCode.SUCCESS;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.hccake.ballcat.common.core.util.JacksonUtils;
import com.hccake.starte.pay.wx.enums.ResponseCode;
import com.hccake.starte.pay.wx.enums.TradeType;
import java.util.Map;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
/**
* @author lingting 2021/2/1 11:38
*/
@Data
@Accessors(chain = true)
public class WxPayResponse {
public static WxPayResponse of(Map<String, String> res) {
return JacksonUtils.toObj(JacksonUtils.toJson(res), WxPayResponse.class).setRaw(res);
}
/**
* 返回状态码. 此字段是通信标识非交易标识交易是否成功需要查看result_code来判断
*/
@JsonProperty("return_code")
private ResponseCode returnCode;
/**
* 返回信息. 返回信息,如非空,为错误原因: 1.签名失败 2.参数格式校验错误
*/
@JsonProperty("return_msg")
private String returnMsg;
/**
* 应用APPID. 调用接口提交的应用ID
*/
@JsonProperty("appid")
private String appId;
/**
* 商户号. 调用接口提交的商户号
*/
@JsonProperty("mch_id")
private String mchId;
/**
* 设备号. 调用接口提交的终端设备号,
*/
@JsonProperty("device_info")
private String deviceInfo;
/**
* 随机字符串. 微信返回的随机字符串
*/
@JsonProperty("nonce_str")
private String nonceStr;
/**
* 签名. 微信返回的签名,详见签名算法
*/
@JsonProperty("sign")
private String sign;
/**
* 业务结果. SUCCESS/FAIL
*/
@JsonProperty("result_code")
private ResponseCode resultCode;
/**
* 错误代码. 详细参见第6节错误列表
*/
@JsonProperty("err_code")
private String errCode;
/**
* 错误代码描述. 错误返回的信息描述
*/
@JsonProperty("err_code_des")
private String errCodeDes;
/**
* 交易类型. 调用接口提交的交易类型取值如下JSAPINATIVEAPP详细说明见参数规定
*/
@JsonProperty("trade_type")
private TradeType tradeType;
/**
* 预支付交易会话标识. 微信生成的预支付回话标识用于后续接口调用中使用该值有效期为2小时
*/
@JsonProperty("prepay_id")
private String prepayId;
/**
* 沙箱专用密钥
*/
@JsonProperty("sandbox_signkey")
private String sandboxSignKey;
/**
* 原生支付返回的二维码
* @see TradeType#NATIVE
*/
@JsonProperty("code_url")
private String codeUrl;
/**
* h5 支付
* @see TradeType#MWEB
*/
@JsonProperty("mweb_url")
private String mWebUrl;
/**
* 返回的原始数据
*/
private Map<String, String> raw;
}

View File

@@ -0,0 +1,188 @@
package com.hccake.starte.pay.wx.utils;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.RandomUtil;
import com.hccake.starte.pay.wx.constants.WxPayConstant;
import com.hccake.starte.pay.wx.enums.SignType;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* @author lingting 2021/1/26 16:04
*/
@Slf4j
public class WxPayUtil {
public static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
return factory.newDocumentBuilder();
}
public static Document getDocument() throws ParserConfigurationException {
return getDocumentBuilder().newDocument();
}
/**
* map 转 xml 字符串
* @param data map
* @return java.lang.String
* @author lingting 2021-02-01 10:22
*/
public static String mapToXml(Map<String, String> data) throws ParserConfigurationException, TransformerException {
Document document = getDocument();
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();
Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(new DOMSource(document), result);
String output = writer.getBuffer().toString();
try {
writer.close();
}
catch (Exception ex) {
log.error("string 写入流关闭异常");
}
return output;
}
/**
* xml字符串转map
* @param xml xml字符串
* @return java.util.Map<java.lang.String,java.lang.String>
* @author lingting 2021-02-01 11:29
*/
public static Map<String, String> xmlToMap(String xml)
throws ParserConfigurationException, IOException, SAXException {
Map<String, String> data = new HashMap<>(30);
InputStream stream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
Document doc = getDocumentBuilder().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) {
Element element = (Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
}
catch (Exception ex) {
// do nothing
}
return data;
}
/**
* 签名
* @param params 参数
* @param mckKey 密钥
* @return java.lang.String 签名结果
* @author lingting 2021-01-29 18:13
*/
@SneakyThrows
public static String sign(Map<String, String> params, String mckKey) {
SignType st = SignType.of(params.get(WxPayConstant.FIELD_SIGN_TYPE));
Assert.isFalse(st == null, "签名类型不能为空!");
String[] keyArray = params.keySet().toArray(new String[0]);
// 参数key排序
Arrays.sort(keyArray);
// 构建排序后的用于签名的字符串
StringBuilder paramsStr = new StringBuilder();
for (String k : keyArray) {
if (k.equals(WxPayConstant.FIELD_SIGN)) {
continue;
}
// 参数值为空,则不参与签名
if (params.get(k).trim().length() > 0) {
paramsStr.append(k).append("=").append(params.get(k).trim()).append("&");
}
}
paramsStr.append("key=").append(mckKey);
// 签名后的字节
byte[] bytes;
if (st == SignType.MD5) {
final MessageDigest md5 = MessageDigest.getInstance("MD5");
bytes = md5.digest(paramsStr.toString().getBytes(StandardCharsets.UTF_8));
}
else {
final Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec sk = new SecretKeySpec(mckKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(sk);
bytes = mac.doFinal(paramsStr.toString().getBytes(StandardCharsets.UTF_8));
}
// 构建返回值
StringBuilder builder = new StringBuilder();
for (byte b : bytes) {
builder.append(Integer.toHexString((b & 0xFF) | 0x100), 1, 3);
}
return builder.toString().toUpperCase();
}
/**
* 生成随机字符串
* @return java.lang.String
* @author lingting 2021-02-25 14:42
*/
public static String generateNonceStr() {
return RandomUtil.randomString(16);
}
}

View File

@@ -18,5 +18,6 @@
<module>ballcat-extend-kafka-stream</module>
<module>ballcat-extend-pay-virtual</module>
<module>ballcat-extend-pay-ali</module>
<module>ballcat-extend-pay-wx</module>
</modules>
</project>

View File

@@ -4,12 +4,16 @@ import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import com.hccake.starte.pay.ali.AliPay;
import com.hccake.starte.pay.ali.domain.AliPayCallback;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lingting 2021/1/25 15:18
@@ -17,7 +21,8 @@ import java.util.Map;
@RestController
@RequestMapping("ali")
@RequiredArgsConstructor
public class Controller {
public class AliController {
private final AliPay aliPay;

View File

@@ -0,0 +1,44 @@
package com.hccake.sample.pay.wx;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import com.hccake.starte.pay.wx.WxPay;
import com.hccake.starte.pay.wx.response.WxPayCallback;
import com.hccake.starte.pay.wx.response.WxPayOrderQueryResponse;
import com.hccake.starte.pay.wx.utils.WxPayUtil;
import java.math.BigDecimal;
import javax.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lingting 2021/2/25 13:55
*/
@RestController
@RequestMapping("wx")
@RequiredArgsConstructor
public class WxController {
private final WxPay wxPay;
private static final Snowflake snowflake = IdUtil.createSnowflake(1, 1);
@SneakyThrows
@GetMapping
public String notice(HttpServletRequest request) {
String sn = snowflake.nextIdStr();
BigDecimal val = new BigDecimal("0.01");
String ip = "27.115.44.246";
// WxPayResponse response = wxPay.nativePay(sn, val, ip, "商品");
WxPayOrderQueryResponse queryResponse = wxPay.query("1364829305962557441", "");
String data = "**";
WxPayCallback callback = WxPayCallback.of(WxPayUtil.xmlToMap(data));
System.out.println(callback.checkSign(wxPay));
return "";
}
}

View File

@@ -24,6 +24,14 @@ ballcat:
alipay-public-key: '***'
notify-url: https://www.baidu.com
return-url: https://www.baidu.com
wx:
sandbox: false
prod:
app-id: '****'
mch-id: '****'
mck-key: '***'
notify-url: https://www.baidu.com
return-url: https://www.baidu.com
management:
endpoints:

View File

@@ -25,6 +25,10 @@
<groupId>com.hccake</groupId>
<artifactId>ballcat-extend-pay-ali</artifactId>
</dependency>
<dependency>
<groupId>com.hccake</groupId>
<artifactId>ballcat-extend-pay-wx</artifactId>
</dependency>
<dependency>
<groupId>com.hccake</groupId>

View File

@@ -0,0 +1,33 @@
package com.hccake.starter.pay.wx;
import com.hccake.starte.pay.wx.WxPay;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* @author lingting 2021/1/25 11:30
*/
@Slf4j
@RequiredArgsConstructor
@ConditionalOnClass(WxPay.class)
@EnableConfigurationProperties(WxPayProperties.class)
public class WxPayAutoConfiguration {
@Bean
@ConditionalOnMissingBean(WxPay.class)
public WxPay wxPay(WxPayProperties properties) {
// 获取配置
WxPayProperties.Config config = properties.getSandbox() ? properties.getDev() : properties.getProd();
WxPay wxPay = new WxPay(config.getAppId(), config.getMchId(), config.getMckKey(), properties.getSandbox());
wxPay.setReturnUrl(config.getReturnUrl());
wxPay.setNotifyUrl(config.getNotifyUrl());
return wxPay;
}
}

View File

@@ -0,0 +1,43 @@
package com.hccake.starter.pay.wx;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author lingting 2021/1/25 11:29
*/
@Data
@ConfigurationProperties(prefix = "ballcat.pay.wx")
public class WxPayProperties {
/**
* 是否使用沙箱, 如果为 false 则使用 prod的配置初始化支付信息, 否则使用dev配置进行初始化
*/
private Boolean sandbox = false;
/**
* 线上环境配置
*/
public Config prod;
/**
* 沙箱配置.
*/
public Config dev;
@Data
public static class Config {
private String appId;
private String mchId;
private String mckKey;
private String returnUrl;
private String notifyUrl;
}
}

View File

@@ -1,3 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hccake.starter.pay.virtual.VirtualPayAutoConfiguration,\
com.hccake.starter.pay.ali.AliPayAutoConfiguration
com.hccake.starter.pay.ali.AliPayAutoConfiguration,\
com.hccake.starter.pay.wx.WxPayAutoConfiguration