目录
第三弹 申请退款
成果展示:
1.退款设计思路
2.申请退款
3.微信退款回调
3.1controller
3.1.1 微信退款controller
3.1.2验证回调类 (和微信支付回调验证一样 如果看了之前的可以不用写)
3.1.3 service 退款回调验证
第三弹 申请退款
当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将在收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。
(这些要根据直接项目的情况按需来安排)
成果展示:
1.退款设计思路
微信官方请求示例:
{
"transaction_id": "1217752501201407033233368018",
"out_refund_no": "1217752501201407033233368018",
"reason": "商品已售完",
"notify_url": "https://weixin.qq.com",
"funds_account": "AVAILABLE",
"amount": {
"refund": 888,
"from": [
{
"account": "AVAILABLE",
"amount": 444
}
],
"total": 888,
"currency": "CNY"
},
"goods_detail": [
{
"merchant_goods_id": "1217752501201407033233368018",
"wechatpay_goods_id": "1001",
"goods_name": "iPhone6s 16G",
"unit_price": 528800,
"refund_amount": 528800,
"refund_quantity": 1
}
]
}
2.申请退款
/**
* 微信退款
* 注意:1、交易时间超过一年的订单无法提交退款
* 2、微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
* 3、错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
* 4、每个支付订单的部分退款次数不能超过50次
* 5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
* 6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
* 7、一个月之前的订单申请退款频率限制为:5000/min
* 8、同一笔订单多次退款的请求需相隔1分钟
* @param wxConfig 微信商家配置
* @param orderId 订单id
* @return String
* @author zhangjunrong
* @date 2022/4/21 15:11
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String refundOrder(ToolWxConfig wxConfig, Long orderId) {
TicketOrderReturn refundOrder = (TicketOrderReturn) redisUtil.get(WxRedisKey.WX_REFUND_ORDER + orderId);
try {
//通过redis 中获取订单相应的信息
if (ObjectUtil.isNotEmpty(refundOrder)) {
log.info("微信退款=====redis记录信息========={}==",refundOrder.toString());
//1.请求配置参数
HttpPost httpPost = new HttpPost(WxApiType.REFUND_ORDER.getValue());
//格式配置
//格式配置
httpPost.addHeader(WechatPayHttpHeaders.ACCEPT, WechatPayHttpHeaders.APPLICATION_JSON);
httpPost.addHeader(WechatPayHttpHeaders.CONTENT_TYPE, WechatPayHttpHeaders.APPLICATION_JSON_UTF);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode rootNode = objectMapper.createObjectNode();
//2.配置参数 订单商户号 退款订单号 微信订单号(微信生成) 退款回调地址(与下单回调地址不一样) 金额信息 amount: 原订单金额 total 退款金额 refund (单位都是分) 退款币种 CNY 人民币
//商户订单号 下单时生成
rootNode.put(WXOrderConstant.OUT_TRADE_NO, refundOrder.getOrderSn())
//微信支付系统生成的订单号(微信生成) 下单时生成
.put(WXOrderConstant.TRANSACTION_ID, refundOrder.getTransactionId())
//微信支付系统生成的订单号 下单时生成(系统生成)
.put(WXOrderConstant.OUT_REFUND_NO, refundOrder.getOrderRefundSn())
//退款回调地址
.put(WXOrderConstant.NOTIFY_URL, wxConfig.getRefNotifyUrl());
//金额信息 amount: 原订单金额 total 退款金额 refund (单位都是分)
rootNode.putObject(WXOrderConstant.AMOUNT)
//现阶段 total==refund 不支持部分退款
//原订单金额 total
.put(WXOrderConstant.AMOUNT_TOTAL, refundOrder.getRefundMoney().movePointRight(SystemConstant.NUM_TWO).intValue())
//退款金额 refund
.put(WXOrderConstant.AMOUNT_REFUND, refundOrder.getRefundMoney().movePointRight(SystemConstant.NUM_TWO).intValue())
//退款币种 CNY 人民币
.put(WXOrderConstant.AMOUNT_CURRENCY, wxConfig.getCurrency());
objectMapper.writeValue(bos, rootNode);
//3.调用微信退款接口
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
//接口返回值
CloseableHttpResponse response = WxPayUtil.getHttpClient(wxConfig, WxPayUtil.getVerifier(wxConfig)).execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
log.info("微信申请退款返回结果" + "response:" + bodyAsString);
JsonNode refundNode = objectMapper.readTree(bodyAsString);
// 修改订单退款状态 微信退款入数据库 库存恢复
//1.修改订单退款状态
String transactionId = refundNode.get(WXOrderConstant.TRANSACTION_ID).textValue();
String status = refundNode.get(WXOrderConstant.STATUS).textValue();
Boolean flag = iTicketOrderService.updateOrderStatus(transactionId, status);
log.info("微信退款======母订单状态修改=={}===",flag);
//2.微信退款入数据库
Boolean insertRefund = iTicketOrderReturnService.insertRefund(refundNode, refundOrder);
log.info("微信退款======退款订单入数据库=={}===",insertRefund);
//3.库存恢复 实现 通过微信支付id(微信生成) 查询出母单id 根据母单id查询出所有子单 让子单库存恢复
iTicketOrderService.restoreStock(transactionId);
return status;
}
} catch (Exception e) {
log.info("微信退款" + refundOrder.getOrderSn() + "失败");
throw new YqsException(MessageEnum.NOT_REFUND.getCode(),"退款失败,请联系客服解决");
}
return null;
}
3.微信退款回调
3.1controller
3.1.1 微信退款controller
@PostMapping("/wechatPayCallback")
@ApiOperation("支付回调给微信确认")
@ApiIgnore
public String wechatCallback(HttpServletRequest request) {
ToolWxConfig wxConfig = iToolWxConfigService.find();
log.info("微信退款回调通知调用=============================");
Gson gson = new Gson();
Map<String,String> result = new HashMap(SystemConstant.NUM_16);
result.put("code", "FAIL");
result.put("message","失败");
try {
//微信回调信息校验
// 构建request,传入必要参数
Notification notification = WxPayUtil.verifyBack(request, wxConfig);
log.info("=================微信验证签名成功=======成功时间=={}=====",notification.getCreateTime());
// 思路: 验证订单 订单号是否存在 订单状态 通过缓存来做到 一回调验证多订单的类型
// 生成订单的时候 把订单信息放入缓存中 order:key key为订单号 30min 通过获取 订单消息做到 快速验证 插入操作 用if 进行
if (iToolWxConfigService.verifyCreateOrder(notification.getDecryptData())) {
log.info("==============================微信退款成功订单=====================================");
result.put("code", WXOrderConstant.WX_BACK_OK);
result.put("message", "支付回调成功");
}
} catch (ValidationException | ParseException | IOException e) {
log.error("微信支付回调失败验证" + e);
}
log.info("微信返回结果"+result);
return gson.toJson(result);
}
3.1.2验证回调类 (和微信支付回调验证一样 如果看了之前的可以不用写)
/**
*回调验证
* @param request 微信回调请求
* @param wxConfig 微信基本配置信息
* @return String
* @author zhangjunrong
* @date 2022/4/21 15:02
*/
public static Notification verifyBack(HttpServletRequest request, ToolWxConfig wxConfig) throws IOException, ValidationException, ParseException {
//应答报文主体
BufferedReader br = request.getReader();
String str;
StringBuilder builder = new StringBuilder();
while ((str = br.readLine()) != null) {
builder.append(str);
}
// 构建request,传入必要参数
//参数 1.微信序列号 2.应答随机串 3.应答时间戳 4.应答签名 5.应答报文主体
NotificationRequest notificationRequest = new NotificationRequest.Builder()
.withSerialNumber(request.getHeader(WechatPayHttpHeaders.WECHATPAY_SERIAL))
.withNonce(request.getHeader(WechatPayHttpHeaders.WECHATPAY_NONCE))
.withTimestamp(request.getHeader(WechatPayHttpHeaders.WECHATPAY_TIMESTAMP))
.withSignature(request.getHeader(WechatPayHttpHeaders.WECHATPAY_SIGNATURE))
.withBody(builder.toString())
.build();
NotificationHandler handler = new NotificationHandler(WxPayUtil.getVerifier(wxConfig), wxConfig.getApiV3key().getBytes(StandardCharsets.UTF_8));
// 验签和解析请求体
log.info("验签和解析请求体==============================开始验证==============================");
Notification notification = handler.parse(notificationRequest);
Assert.assertNotNull(notification);
return notification;
}
3.1.3 service 退款回调验证
/**
*微信支付回调验证判定 核对成功 数据异步入库
* @param
* @param decryptOrder
* @return Boolean
* @author zhangjunrong
* @date 2022/5/3 8:23
*/
@Override
public Boolean verifyCreateOrder(String decryptOrder) {
//在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
//实现: 加入一把可重入锁
if (reentrantLock.tryLock()) {
try {
log.info("===================================进入微信支付回调核对订单中========================================");
ObjectMapper objectMapper = new ObjectMapper();
//微信回调 解密后 信息
JsonNode node = objectMapper.readTree(decryptOrder);
//获取订单商户号
String orderNo = node.get(WXOrderConstant.OUT_TRADE_NO).textValue();
//1.获取redis中的订单信息
OrderTotalRedisRO totalRedisRO = (OrderTotalRedisRO) redisUtil.get(SystemConstant.ORDER_TOTAL + orderNo);
//1.1微信 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
//实现方法: 通过订单状态来 判定是否要进行判定 出未支付以外的 都返回 结果 通过缓存获取到支付状态 防止微信重复调用该方法
//如果回调 缓存中记录清除说明 入库判定等等成功 直接返回true
if (ObjectUtil.isEmpty(totalRedisRO)) {
return true;
}
log.info(node.get(WXOrderConstant.OUT_TRADE_NO) + "订单回调信息记录:订单状态:" + orderNo);
//2.如果回调 支付类型为成功 核对金额 入数据库
//获取支付状态
String tradeState = node.get(WXOrderConstant.TRADE_STATE).textValue();
if (StrUtil.equals(WXOrderConstant.WX_BACK_OK, tradeState)) {
//redis缓存中的金额
int redisTotal = totalRedisRO.getTicketOrder().getPayMoney().movePointRight(SystemConstant.NUM_TWO).intValue();
//校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
//缓存中存入的用户支付金额 totalRedisRO.getTicketOrder().getTotalMoney().movePointRight(SystemConstant.NUM_TWO).intValue()
if (WxPayUtil.verifyMoney(node, redisTotal)) {
//3.对应的数据入库
log.info("redis入数据库信息======================" + totalRedisRO);
if (!ObjectUtil.isEmpty(totalRedisRO)) {
//缓存放入一个状态 表明已操作该订单 存放200秒
// 支付成功就把redis中缓存记录清除
totalRedisRO.getTicketOrder().setOrderStatus(SystemConstant.NUM_ONE);
redisUtil.del(SystemConstant.ORDER_TOTAL + orderNo);
//订单入库
iTicketOrderService.createAllTicket(totalRedisRO, node.get(WXOrderConstant.TRANSACTION_ID).textValue());
}
}
//为什么没有插入成功也返回true?
//因为就算数据库没有入成功 但是金额 订单校验等等的都通过
//说明数据库入库失败
//如果入库失败 让用户联系客服接入管理 [钱一定要收下来]
return true;
}
} catch (Exception e) {
log.error("订单支付异常===>订单回调信息记录:订单状态:" + decryptOrder);
}finally {
//释放锁
reentrantLock.unlock();
}
}
return false;
}