接上一篇Java微信支付-申请退款API,本篇在上篇文章的基础上讲述调用申请退款API后退款成功之后微信异步回调通知
下文中所需配置、类都在以请查看以上链接内容。
在调用微信支付-申请退款API时,会传递
notify_url
这个参数给微信,这个参数是退款成功之后微信端会向此地址进行通知,我们应该在接受到微信发来的通知时进行验签确保安全性。
注意:notify_url
必须为外网可访问的url,不能携带参数。测试时可以使用内网穿透
进行测试,这个东西在此我就不赘述了,需要的朋友自行Google。
/**
* 微信支付Controller
*
* @create: 2019-10-10 15:40
* @author: Sun
*/
@RequestMapping(value = "/wxpay")
@RestController
@Slf4j
public class WxPayController {
@Autowired
private WxPayService wxPayService;
/**
* 微信退款异步回调接口
*
* @return
*/
@RequestMapping(value = "/refund/async/notify")
public void refundAsyncNotify(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
log.info("[refundAsyncNotify]");
String resultXml = wxPayService.refundAsyncNotify(httpServletRequest);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(httpServletResponse.getOutputStream());
bufferedOutputStream.write(resultXml.getBytes());
bufferedOutputStream.flush();
bufferedOutputStream.close();
}
}
/**
* 微信支付接口
*
* @create: 2019-10-10 15:40
* @author: Sun
*/
public interface WxPayService {
/**
* 微信退款异步通知验证签名
* @param httpServletRequest
* @return
*/
String refundAsyncNotify(HttpServletRequest httpServletRequest);
}
/**
* 微信支付实现
*
* @author: Sun
* @create: 2019-10-10 17:54
* @version: v1.0
*/
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Autowired
private MyWxPayConfig myWxPayConfig;
@Autowired
private WxDecodeUtil wxDecodeUtil;
/**
* 微信响应字段
*/
private static final String RETURN_CODE = "return_code";
@Override
public String refundAsyncNotify(HttpServletRequest httpServletRequest) {
String returnXmlMessage = null;
String notifyXmlData = null;
try {
notifyXmlData = readXmlFromStream(httpServletRequest);
Map<String, String> notifyMapData = WXPayUtil.xmlToMap(notifyXmlData);
log.info("[refundAsyncNotify] [xml转换为map数据成功] [notifyMapData:{}]", notifyMapData);
if (WXPayConstants.SUCCESS.equals(notifyMapData.get(RETURN_CODE))) {
// 获得加密信息
String reqInfo = notifyMapData.get("req_info");
// 进行AES解密 获取req_info中包含的相关信息(解密失败会抛出异常)
String refundDecryptedData = wxDecodeUtil.decryptData(reqInfo);
Map<String, String> reqInfoMap = WXPayUtil.xmlToMap(refundDecryptedData);
log.info("[refundAsyncNotify] [reqInfo解密成功] [reqInfoMap:{}]", reqInfoMap);
// TODO 订单退款成功后相关业务逻辑...
// 组装返回给微信的xml数据
returnXmlMessage = setReturnXml(WXPayConstants.SUCCESS, "OK");
log.info("[refundAsyncNotify] [out_trade_no:{}] [out_refund_no:{}] [退款异步消息处理成功:{}]",
reqInfoMap.get("out_trade_no"), reqInfoMap.get("out_refund_no"), returnXmlMessage);
} else {
returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "return_code不为success");
}
} catch (IOException e) {
log.error("[refundAsyncNotify] [读取微信服务器返回流中xml数据时发生异常:{}] ", ExceptionUtils.getStackTrace(e));
returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "An exception occurred while reading the WeChat server returning xml data in the stream.");
} catch (Exception e) {
log.error("[refundAsyncNotify [处理异常]] [xml数据:{}] [异常:{}] ", notifyXmlData, ExceptionUtils.getStackTrace(e));
returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "Refund successful, exception occurred during asynchronous notification processing.");
log.warn("[refundAsyncNotify] [退款异步消息处理失败:{}]", returnXmlMessage);
}
return returnXmlMessage;
}
/**
* 设置返回给微信服务器的xml信息
*
* @param returnCode
* @param returnMsg
* @return
*/
private String setReturnXml(String returnCode, String returnMsg) {
return "<xml><return_code><![CDATA[" + returnCode + "]]></return_code><return_msg><![CDATA[" + returnMsg + "]]></return_msg></xml>";
}
/**
* 从流中读取微信返回的xml数据
*
* @param httpServletRequest
* @return
* @throws IOException
*/
private String readXmlFromStream(HttpServletRequest httpServletRequest) throws IOException {
InputStream inputStream = httpServletRequest.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
final StringBuffer sb = new StringBuffer();
String line = null;
try {
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
} finally {
bufferedReader.close();
inputStream.close();
}
return sb.toString();
}
}
/**
* 微信解密工具类(AES-256-ECB解密 PKCS7Padding)
* 解密方式官方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=10#menu1
* 解密步骤如下:
* (1)对加密串A做base64解码,得到加密串B
* (2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 )
* (3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)
*/
@Component
public class WxDecodeUtil {
private String algorithm = "AES";
private String algorithmModePadding = "AES/ECB/PKCS7Padding";
private String key;
SecretKeySpec secretKey;
private boolean initialized = false;
/**
* AES解密
* @param base64Data
* @return
* @throws Exception
*/
public String decryptData(String base64Data) throws Exception {
initialize();
// 获取解码器实例,"BC"指定Java使用BouncyCastle库里的加/解密算法。
Cipher cipher = Cipher.getInstance(algorithmModePadding, "BC");
// 使用秘钥并指定为解密模式初始化解码器
cipher.init(Cipher.DECRYPT_MODE, secretKey);
// cipher.doFinal(byte[] b)在单部分操作中加密或解密数据,或完成多部分操作。 根据此秘钥的初始化方式,对数据进行加密或解密。
return new String(cipher.doFinal(Base64.decode(base64Data)));
}
/**
* 安全提供者列表中注册解密算法提供者,这个加载过程还挺慢的,有时候要好几秒,只需要加载一次就能一直使用。
*/
private void initialize() {
if (initialized) {
return;
}
Security.addProvider(new BouncyCastleProvider());
initialized = true;
}
/**
* 构造方法(容器初始化时从配置文件中获取key,在全局中维护一个唯一的SecretKeySpec)
* @param key
*/
public WxDecodeUtil(@Value("${wxconfig.key}") String key) {
this.key = key;
// 转化成JAVA的密钥格式
secretKey = new SecretKeySpec(WxMd5Util.MD5Encode(key, "UTF-8").toLowerCase().getBytes(), algorithm);
}
}