前言
微信登录网页授权与APP授权
微信JSAPI支付
微信APP支付
微信APP和JSAPI退款
支付宝手机网站支付
支付宝APP支付
支付宝退款
以上我都放到个人公众号,搜一搜:JAVA大贼船,文末有公众号二维码!觉得个人以后开发会用到的可以关注一下哦!少走点弯路…
官方文档
APP申请退款和JSAPI申请退款可以共用,各自的文档基本一样
微信APP申请退款文档
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_4&index=6
微信JSAPI申请退款文档
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
代码实现
引入依赖
<!-- 微信支付 -->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
配置参数
application.yml
# 微信相关配置
wx:
#商户 ID(微信支付平台-账户中心-个人信息)
MCH_ID:
# APP_ID(微信开放平台或公众号查找)
A_APP_ID:
# 秘钥(微信开放平台或公众号查找)
A_APP_SECRET:
# APP_ID(微信开放平台或公众号查找)
H_APP_ID:
# 秘钥(微信开放平台或公众号查找)
H_APP_SECRET:
# 消息加解密所用到的解密串(微信公众号-基本配置查找)
H_ENCODINGAESKEY:
# token(微信公众号-基本配置查找)
H_TOKEN:
# 支付秘钥KEY(微信支付平台-账户中心-api安全-api秘钥)
KEY:
# 支付商户证书所载目录(微信支付平台-账户中心-api安全-API证书)
CERT_PATH:
#支付成功回调地址
WX_REFUND_CALLBACK_URL:
YmlParament
@Component
@Data
public class YmlParament {
/*微信相关字段*/
@Value("${wx.A_APP_ID}")
private String a_app_id;
@Value("${wx.A_APP_SECRET}")
private String a_app_secret;
@Value("${wx.MCH_ID}")
private String mch_id;
@Value("${wx.KEY}")
private String key;
@Value("${wx.CERT_PATH}")
private String cert_path;
@Value("${wx.H_APP_ID}")
private String h_app_id;
@Value("${wx.H_APP_SECRET}")
private String h_app_secret;
@Value("${wx.H_ENCODINGAESKEY}")
private String h_encodingaeskey;
@Value("${wx.H_TOKEN}")
private String h_token;
@Value("${wx.WX_CALLBACK_URL}")
private String wx_refund_callback_url;
微信申请退款
- 初始化微信配置
@Component
public class WxConfig {
@Autowired
private YmlParament ymlParament;
/**
* 初始化微信配置
* @throws Exception
*/
@Bean(autowire = Autowire.BY_NAME,value = WxParament.H5_WX_PAY)
public WXPay setH5WXPay() throws Exception {
return new WXPay(new WxPayConfig(
ymlParament.getCert_path(),
ymlParament.getH_app_id(),
ymlParament.getMch_id(),
ymlParament.getkey()));
}
}
@Bean(autowire = Autowire.BY_NAME,value = WxParament.APP_WX_PAY)
public WXPay setAppWXPay() throws Exception {
return new WXPay(new WxPayConfig(
ymlParament.getCert_path(),
ymlParament.getA_app_id(),
ymlParament.getMch_id(),
ymlParament.getkey()));
}
WxPayConfig
public class WxPayConfig implements WXPayConfig {
private byte[] certData;
private String appID;
private String mchID;
private String key;
public WxPayConfig(String certPath, String appID,String mchID,String key) throws Exception {
File file = new File(certPath);
InputStream certStream = new FileInputStream(file);
this.certData = new byte[(int) file.length()];
certStream.read(this.certData);
certStream.close();
this.appID = appID;
this.mchID = mchID;
this.key = key;
}
}
- 微信申请退款接口,关键代码(服务层,业务逻辑略)
@Resource(name = WxParament.H5_WX_PAY)
private WXPay wxH5Pay;
@Resource(name = WxParament.APP_WX_PAY)
private WXPay wxAppPay;
@Transactional
@Override
public void wxRefund(String refundNo, String sendOrderNo,String orderAmount,String refundAmount) throws Exception {
checkWxRefund();
//微信方式退款
Map<String, String> data = new HashMap<String, String>();
data.put("nonce_str", WXPayUtil.generateNonceStr());
data.put("sign_type", "MD5");
//商户订单号
data.put("out_trade_no", sendOrderNo);
//商户退款单号(多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。)
data.put("out_refund_no", refundNo);
//订单金额
data.put("total_fee", orderAmount);
//退款金额
data.put("refund_fee", refundAmount);
data.put("notify_url", ymlParament.getWx_refund_return_url());
data.put("refund_desc","我要退款");
//这里的payInfo是保存着支付记录,从而找到这笔订单的支付方式
Map<String, String> payRefundMap = WxPay.payRefund(payInfo.getPayWayInfo()== PayInfoEums.PAY_WAY_INFO_1.getValue()?wxH5Pay:wxAppPay, data);
if ("FAIL".equals(payRefundMap.get("result_code")) ||
"FAIL".equals(payRefundMap.get("return_code")) ) {
//微信申请退款失败
}
}
private void checkWxRefund(){
//1、交易时间超过一年的订单无法提交退款
// 2、微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。
// 申请退款总金额不能超过订单金额。
// * 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
//3、请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次
// * 错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
//4、每个支付订单的部分退款次数不能超过50次
}
WxPay
/**
* 申请退款 参考链接:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
* 注意退款结果通知,此接口为异步,只能判断是否调用成功,不能判断是否退款成功,退款成功需要参考回调:
* https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=10
*/
public static Map<String, String> payRefund(WXPay wp, Map<String, String> parament) throws Exception {
return wp.refund(parament);
}
退款结果通知
*当商户申请的退款有结果后,微信会把相关结果发送给商户,商户需要接收处理,并返回应答。 * 对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败, * 微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率, * 但微信不保证通知最终能成功(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)。 * 注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 * 推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态, * 判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。 * 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。 * 特别说明:退款结果对重要的数据进行了加密,商户需要用商户秘钥进行解密后才能获得结果通知的内容
@Autowired
private YmlParament ymlParament;
@ApiOperation("微信退款通知")
@PostMapping("callBackWxRefund")
public String callBackWxRefund(HttpServletRequest request) throws Exception {
// 1、获取参数
Map<String, String> params = WxUtils.getMapByRequest(request);
log.info("微信退款回调回来啦!!!!参数为:" + params);
//2、解密方式
//解密步骤如下:
//(1)对加密串A做base64解码,得到加密串B
//(2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
//(3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
try {
Map<String, String> reqInfo = WxUtils.decryptData(params.get("req_info"), ymlParament.getKey());
// 4、把订单的相关详情查询出来
//5、判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。
checkCallbackWxRefund(params,reqInfo);
//退款成功
//业务逻辑
} catch (Exception e) {
e.printStackTrace();
}
}
return wxPaySuccess();
}
private void checkCallbackWxRefund(Map<String,String> params,Map<String, String> reqInfo) throws Exception {
if (!"SUCCESS".equals(params.get("refund_status"))){
throw new Exception("微信退款失败");
}
if (StringUtils.isEmpty(reqInfo)){
throw new Exception("微信退款参数解密失败");
}
//校验业务逻辑
}
private static String wxPaySuccess() throws Exception {
Map<String, String> succResult = new HashMap();
succResult.put("return_code", "SUCCESS");
succResult.put("return_msg", "OK");
return WXPayUtil.mapToXml(succResult);
}
工具类
WxUtils
/**
* 密钥算法
*/
private static final String ALGORITHM = "AES";
/**
* 加解密算法/工作模式/填充方式
*/
private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS5Padding";
/**
* 获取微信过来的请求参数
* @param request
* @return
* @throws Exception
*/
public static Map<String, String> getMapByRequest(HttpServletRequest request) throws Exception{
InputStream inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
Map<String, String> ret= WXPayUtil.xmlToMap(new String(outSteam.toByteArray(), "utf-8"));
outSteam.close();
inStream.close();
return ret;
}
/**
* @param base64Data 需要进行base64解码的字符串
* @param password 商户key
* @return java.util.Map<java.lang.String,java.lang.String>
* @author aotezi
* @date 2020/4/7 15:52
*/
public static Map<String, String> decryptData(String base64Data,String password) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
SecretKeySpec key = new SecretKeySpec(MD5(password).toLowerCase().getBytes(), ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decode = decode(base64Data);
byte[] doFinal = cipher.doFinal(decode);
Map<String, String> xmlToMap = WXPayUtil.xmlToMap(new String(doFinal, "utf-8"));
return xmlToMap;
}
/**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
private String MD5(String data) {
try {
byte[] array = MessageDigest.getInstance("MD5").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();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 解码
* @param encodedText
* @return
*/
private static byte[] decode(String encodedText){
final Base64.Decoder decoder = Base64.getDecoder();
return decoder.decode(encodedText);
}