提现转账

This commit is contained in:
墨大叔 2024-08-12 18:02:43 +08:00
parent 55bced1bb0
commit 57e507e70a
37 changed files with 934 additions and 404 deletions

View File

@ -52,6 +52,9 @@ public class WxPayConfig {
@Value("${wx.pay.merchantSerialNumber}")
private String merchantSerialNumber;
@Value("${wx.pay.transferNotifyUrl}")
private String transferNotifyUrl;
@Bean
public AppService appService () {
// 初始化商户配置

View File

@ -1,4 +1,4 @@
package com.ruoyi.common.pay.wx.domain;
package com.ruoyi.common.pay.wx.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -10,7 +10,7 @@ import lombok.Getter;
*/
@Getter
@AllArgsConstructor
public enum NotifyEventType {
public enum WxNotifyEventType {
TRANSACTION_SUCCESS("TRANSACTION.SUCCESS", "支付成功"),
MCHTRANSFER_BATCH_FINISHED("MCHTRANSFER.BATCH.FINISHED", "商户转账批次完成通知"),

View File

@ -1,6 +1,9 @@
package com.ruoyi.common.pay.wx.domain;
package com.ruoyi.common.pay.wx.domain.enums;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -11,7 +14,7 @@ import lombok.Getter;
*/
@AllArgsConstructor
@Getter
public enum TransferBatchStatus {
public enum WxTransferBatchStatus {
WAIT_PAY("WAIT_PAY", "待付款确认"),
ACCEPTED("ACCEPTED", "已受理"),
PROCESSING("PROCESSING", "转账中"),
@ -22,12 +25,16 @@ public enum TransferBatchStatus {
private final String status;
private final String msg;
public static TransferBatchStatus parse(String status) {
for (TransferBatchStatus obj : TransferBatchStatus.values()) {
public static WxTransferBatchStatus parse(String status) {
for (WxTransferBatchStatus obj : WxTransferBatchStatus.values()) {
if (Objects.equals(obj.getStatus(), status)) {
return obj;
}
}
throw new RuntimeException("不存在值为" + status + "的状态");
return null;
}
}
public static List<String> asList(WxTransferBatchStatus...statuses) {
return Arrays.stream(statuses).map(WxTransferBatchStatus::getStatus).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,30 @@
package com.ruoyi.common.pay.wx.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author wjh
* 2024/8/12
*/
@Getter
@AllArgsConstructor
public enum WxTransferDetailStatus {
/**
* INIT 初始状态 明细的初始状态
* WAIT_PAY 待商户确认 当前明细等待商户管理员确认付款 商户管理员确认转账或撤销转账
* PROCESSING 转账中 当前明细正在处理中转账结果尚未明确 查询明细单确认明细处理结果
* SUCCESS 转账成功 当前明细转账已成功 向用户展现转账结果
* FAIL 转账失败 当前明细转账已失败 确认失败原因并决定是否重新发起当前明细转账并非整个转账批次
*/
WAIT_PAY("WAIT_PAY", "待商户确认"),
PROCESSING("PROCESSING", "转账中"),
SUCCESS("SUCCESS", "转账成功"),
FAIL("FAIL", "转账失败"),
INIT("INIT", "初始状态");
private final String status;
private final String msg;
}

View File

@ -12,7 +12,7 @@ import java.math.BigDecimal;
*/
@AllArgsConstructor
@Getter
public enum TransferScene {
public enum WxTransferScene {
WITHDRAW(new BigDecimal(500), "提现");

View File

@ -0,0 +1,17 @@
package com.ruoyi.common.pay.wx.domain.request;
import com.google.gson.annotations.SerializedName;
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferRequest;
import lombok.Data;
/**
* @author wjh
* 2024/8/12
*/
@Data
public class MyInitiateBatchTransferRequest extends InitiateBatchTransferRequest {
@SerializedName("notify_url")
private String notifyUrl;
}

View File

@ -1,83 +1,116 @@
package com.ruoyi.common.pay.wx.service;
import com.ruoyi.common.pay.wx.domain.BatchTransferAble;
import com.ruoyi.common.pay.wx.domain.Payable;
import com.ruoyi.common.pay.wx.domain.RefundAble;
import com.ruoyi.common.pay.wx.domain.enums.TransferScene;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.config.WxPayConfig;
import com.ruoyi.common.pay.wx.domain.*;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferResponse;
import com.wechat.pay.java.service.transferbatch.model.TransferDetailInput;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.List;
/**
* 微信支付服务接口
* 微信支付服务
* @author
* 2024/3/11
*/
public interface WxPayService {
@Service
@Slf4j
public class WxPayService {
PrepayWithRequestPaymentResponse prepayWithRequestPayment(Payable payable);
@Autowired
private JsapiService jsapiService;
PrepayWithRequestPaymentResponse prepayWithRequestPayment(String billNo, BigDecimal money, String description, String wxOpenId, String attach);
@Autowired
private JsapiServiceExtension jsapiServiceExtension;
@Autowired
private WxPayConfig wxPayConfig;
@Autowired
private RefundService refundService;
private static final String CNY = "CNY";
public PrepayWithRequestPaymentResponse prepayWithRequestPayment(Payable payable) {
return this.prepayWithRequestPayment(payable.payableOutTradeNo(), payable.payableMoney(), payable.payableDescription(), payable.payableOpenId(), payable.payableAttach());
}
public PrepayWithRequestPaymentResponse prepayWithRequestPayment(String billNo, BigDecimal money, String description, String wxOpenId, String attach) {
// 获取JSAPI所需参数
PrepayRequest request = new PrepayRequest();
request.setAmount(getAmount(money));
request.setOutTradeNo(billNo);
request.setAppid(wxPayConfig.getAppId());
request.setMchid(wxPayConfig.getMerchantId());
request.setDescription(description);
request.setNotifyUrl(wxPayConfig.getNotifyUrl());
request.setPayer(getPayer(wxOpenId));
request.setAttach(attach);
return jsapiServiceExtension.prepayWithRequestPayment(request);
}
/**
* 关闭订单
* 关闭支付订单
* @param billNo 平台订单编号
*/
void closeOrder(String billNo);
public void closeOrder(String billNo) {
CloseOrderRequest request = new CloseOrderRequest();
request.setMchid(wxPayConfig.getMerchantId());
request.setOutTradeNo(billNo);
jsapiService.closeOrder(request);
}
/**
* 通过微信订单id查询订单信息
* @param prePayId 微信订单id
* @return 订单信息
*/
Transaction queryOrderById(String prePayId);
public Transaction queryOrderById(String prePayId) {
QueryOrderByIdRequest request = new QueryOrderByIdRequest();
request.setMchid(wxPayConfig.getMerchantId());
request.setTransactionId(prePayId);
return jsapiService.queryOrderById(request);
}
/**
* 通过订单编号查询订单信息
* @param billNo 订单编号
* @return 订单信息
*/
Transaction queryOrderByOutTradeNo(String billNo);
boolean isSuccess(Transaction transaction);
/**
* 转账到零钱
*/
InitiateBatchTransferResponse batchTransfer(BatchTransferAble batchTransferAble);
/**
* 微信商户支付通知
*/
void wxTransferNotify(HttpServletRequest request);
/**
* 构造给一个用户转账所需的明细列表将限额分解成多个明细
* @param totalAmount 转账总金额
* @param openId 微信用户openId
* @param transferScene 转账场景
*/
List<TransferDetailInput> buildTransferDetailList(BigDecimal totalAmount, String openId, TransferScene transferScene);
public Transaction queryOrderByOutTradeNo(String billNo) {
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
request.setMchid(wxPayConfig.getMerchantId());
request.setOutTradeNo(billNo);
return jsapiService.queryOrderByOutTradeNo(request);
}
/**
* 发起退款
*
* @param refund
*/
Refund refund(RefundAble refund);
public Refund refund(RefundAble refund) {
CreateRequest request = new CreateRequest();
request.setOutTradeNo(refund.refundOutTradeNo());
request.setOutRefundNo(refund.refundOutRefundNo());
request.setReason(refund.refundReason());
request.setAmount(refund.refundAmount());
request.setNotifyUrl(wxPayConfig.getRefundNotifyUrl());
log.info("【退款】请求微信参数: {}", JSON.toJSONString(request));
Refund res = refundService.create(request);
log.info("【退款】微信返回结果:【{}】",JSON.toJSONString(refund));
return res;
}
/**
* 验签并解析
*/
<T> T checkAndParse(HttpServletRequest request, String body, Class<T> clazz);
private Payer getPayer(String openId) {
Payer payer = new Payer();
payer.setOpenid(openId);
return payer;
}
private Amount getAmount(BigDecimal money) {
Amount amount = new Amount();
amount.setTotal(money.intValue());
amount.setCurrency(CNY);
return amount;
}
/**
* 转为业务对象
*/
Transaction toTransaction(HttpServletRequest request);
}

View File

@ -1,254 +0,0 @@
package com.ruoyi.common.pay.wx.service;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.config.WxPayConfig;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.pay.wx.domain.*;
import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.common.utils.SnowFlakeUtil;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.pay.wx.domain.enums.TransferScene;
import com.wechat.pay.java.core.notification.Notification;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import com.wechat.pay.java.service.transferbatch.TransferBatchService;
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferRequest;
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferResponse;
import com.wechat.pay.java.service.transferbatch.model.TransferDetailInput;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 微信支付服务
* @author
* 2024/3/11
*/
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
@Autowired
private JsapiService jsapiService;
@Autowired
private JsapiServiceExtension jsapiServiceExtension;
@Autowired
private WxPayConfig wxPayConfig;
@Autowired
private NotificationParser notificationParser;
@Autowired
private RefundService refundService;
@Autowired
private TransferBatchService transferBatchService;
private static final String CNY = "CNY";
@Override
public PrepayWithRequestPaymentResponse prepayWithRequestPayment(Payable payable) {
return this.prepayWithRequestPayment(payable.payableOutTradeNo(), payable.payableMoney(), payable.payableDescription(), payable.payableOpenId(), payable.payableAttach());
}
@Override
public PrepayWithRequestPaymentResponse prepayWithRequestPayment(String billNo, BigDecimal money, String description, String wxOpenId, String attach) {
// 获取JSAPI所需参数
PrepayRequest request = new PrepayRequest();
request.setAmount(getAmount(money));
request.setOutTradeNo(billNo);
request.setAppid(wxPayConfig.getAppId());
request.setMchid(wxPayConfig.getMerchantId());
request.setDescription(description);
request.setNotifyUrl(wxPayConfig.getNotifyUrl());
request.setPayer(getPayer(wxOpenId));
request.setAttach(attach);
return jsapiServiceExtension.prepayWithRequestPayment(request);
}
/**
* 关闭支付订单
* @param billNo 平台订单编号
*/
@Override
public void closeOrder(String billNo) {
CloseOrderRequest request = new CloseOrderRequest();
request.setMchid(wxPayConfig.getMerchantId());
request.setOutTradeNo(billNo);
jsapiService.closeOrder(request);
}
@Override
public Transaction queryOrderById(String prePayId) {
QueryOrderByIdRequest request = new QueryOrderByIdRequest();
request.setMchid(wxPayConfig.getMerchantId());
request.setTransactionId(prePayId);
return jsapiService.queryOrderById(request);
}
@Override
public Transaction queryOrderByOutTradeNo(String billNo) {
QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
request.setMchid(wxPayConfig.getMerchantId());
request.setOutTradeNo(billNo);
return jsapiService.queryOrderByOutTradeNo(request);
}
@Override
public boolean isSuccess(Transaction transaction) {
return transaction != null && Transaction.TradeStateEnum.SUCCESS.equals(transaction.getTradeState());
}
/**
* 转账到零钱
*/
@Override
public InitiateBatchTransferResponse batchTransfer(BatchTransferAble bill) {
InitiateBatchTransferRequest request = new InitiateBatchTransferRequest();
request.setAppid(wxPayConfig.getAppId());
request.setOutBatchNo(bill.transferOutBatchNo());
request.setBatchName(bill.transferBatchName());
request.setBatchRemark(bill.transferBatchRemark());
request.setTotalAmount(bill.transferTotalAmount());
request.setTotalNum(bill.transferDetailList().size());
request.setTransferDetailList(bill.transferDetailList());
return transferBatchService.initiateBatchTransfer(request);
}
@Override
@Transactional
public void wxTransferNotify(HttpServletRequest request) {
throw new ServiceException("开发中");
// String body = HttpUtils.getBody(request);
// // 解析通知数据
// Notification notification = JSON.parseObject(body, Notification.class);
//
// // 判断是否重复通知重复通知则忽略
// if (isRepeatNotify(notification.getId())) {
// return;
// }
//
// // 商户转账成功通知
// if (NotifyEventType.MCHTRANSFER_BATCH_FINISHED.getValue().equals(notification.getEventType())) {
// // 验签解密并转换成 TransferBatchGet
// TransferBatchGet transferBatchGet = checkAndParse(request, body, TransferBatchGet.class);
// TransactionBill bill = transactionBillService.selectSmTransactionBillByBillNo(transferBatchGet.getOutBatchNo());
// ServiceUtil.assertion(bill == null, "订单不存在");
//
// // 修改订单状态
// if (TransferBatchStatus.FINISHED.getStatus().equals(transferBatchGet.getBatchStatus())) {
// // 提现成功
// transactionBillService.withdrawSuccess(bill.getBillId(), LocalDateTime.now());
// } else if (TransferBatchStatus.CLOSED.getStatus().equals(transferBatchGet.getBatchStatus())) {
// // 提现失败
// transactionBillService.withdrawFailed(bill.getBillId());
// }
// }
}
@Override
public List<TransferDetailInput> buildTransferDetailList(BigDecimal totalAmount, String openId, TransferScene transferScene) {
ServiceUtil.assertion(BigDecimal.ZERO.compareTo(totalAmount) > 0, "转账总金额不允许小于0");
ServiceUtil.assertion(StringUtils.isBlank(openId), "微信openId不允许为空");
ServiceUtil.assertion(transferScene == null, "转账场景不允许为空");
List<TransferDetailInput> transferDetailList = new ArrayList<>();
BigDecimal limitAmount = transferScene.getLimitAmount(); // 单笔限额
BigDecimal payAmount = totalAmount; // 需要转账的金额
while (BigDecimal.ZERO.compareTo(payAmount) < 0) {
BigDecimal detailPayAmount = payAmount.compareTo(limitAmount) > 0 ? limitAmount : payAmount; // 当前明细转账的金额
TransferDetailInput input = new TransferDetailInput();
input.setOutDetailNo(String.valueOf(SnowFlakeUtil.newId()));
input.setTransferAmount(detailPayAmount.longValue());
input.setTransferRemark(transferScene.getRemark());
input.setOpenid(openId);
transferDetailList.add(input);
payAmount = payAmount.subtract(detailPayAmount);
}
return transferDetailList;
}
/**
* 发起退款
*
* @param refund
*/
@Override
public Refund refund(RefundAble refund) {
CreateRequest request = new CreateRequest();
request.setOutTradeNo(refund.refundOutTradeNo());
request.setOutRefundNo(refund.refundOutRefundNo());
request.setReason(refund.refundReason());
request.setAmount(refund.refundAmount());
request.setNotifyUrl(wxPayConfig.getRefundNotifyUrl());
log.info("【退款】请求微信参数: {}", JSON.toJSONString(request));
Refund res = refundService.create(request);
log.info("【退款】微信返回结果:【{}】",JSON.toJSONString(refund));
return res;
}
/**
* 验签并解析
* @param request 请求
* @param body 请求体
* @param clazz 返回值类型
*/
@Override
public <T> T checkAndParse(HttpServletRequest request, String body, Class<T> clazz) {
// 构造 RequestParam
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(request.getHeader("Wechatpay-Serial"))
.nonce(request.getHeader("Wechatpay-Nonce"))
.signature(request.getHeader("Wechatpay-Signature"))
.timestamp(request.getHeader("Wechatpay-Timestamp"))
.body(body)
.build();
// 验签
return notificationParser.parse(requestParam, clazz);
}
@Override
public Transaction toTransaction(HttpServletRequest request) {
// 获取原始报文body
String body = HttpUtils.getBody(request);
// 解析通知数据
Notification notification = JSON.parseObject(body, Notification.class);
// 支付成功通知
if (NotifyEventType.TRANSACTION_SUCCESS.getValue().equals(notification.getEventType())) {
// 验签解密并转换成 Transaction
return checkAndParse(request, body, Transaction.class);
}
return null;
}
private Payer getPayer(String openId) {
Payer payer = new Payer();
payer.setOpenid(openId);
return payer;
}
private Amount getAmount(BigDecimal money) {
Amount amount = new Amount();
amount.setTotal(money.intValue());
amount.setCurrency(CNY);
return amount;
}
}

View File

@ -0,0 +1,60 @@
package com.ruoyi.common.pay.wx.service;
import com.ruoyi.common.config.WxPayConfig;
import com.ruoyi.common.pay.wx.domain.BatchTransferAble;
import com.ruoyi.common.pay.wx.domain.request.MyInitiateBatchTransferRequest;
import com.wechat.pay.java.service.transferbatch.TransferBatchService;
import com.wechat.pay.java.service.transferbatch.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author wjh
* 2024/8/12
*/
@Service
public class WxTransferService {
@Autowired
private WxPayConfig wxPayConfig;
@Autowired
private TransferBatchService transferBatchService;
/**
* 转账到零钱
*/
public InitiateBatchTransferResponse batchTransfer(BatchTransferAble batchTransferAble) {
MyInitiateBatchTransferRequest request = new MyInitiateBatchTransferRequest();
request.setAppid(wxPayConfig.getAppId());
request.setOutBatchNo(batchTransferAble.transferOutBatchNo());
request.setBatchName(batchTransferAble.transferBatchName());
request.setBatchRemark(batchTransferAble.transferBatchRemark());
request.setTotalAmount(batchTransferAble.transferTotalAmount());
request.setTotalNum(batchTransferAble.transferDetailList().size());
request.setTransferDetailList(batchTransferAble.transferDetailList());
request.setNotifyUrl(wxPayConfig.getTransferNotifyUrl());
return transferBatchService.initiateBatchTransfer(request);
}
/** 通过微信批次单号查询批次单 */
public TransferBatchEntity getTransferBatchByNo(GetTransferBatchByNoRequest request) {
return transferBatchService.getTransferBatchByNo(request);
}
/** 通过商家批次单号查询批次单 */
public TransferBatchEntity getTransferBatchByOutNo(GetTransferBatchByOutNoRequest request) {
return transferBatchService.getTransferBatchByOutNo(request);
}
/** 通过微信明细单号查询明细单 */
public TransferDetailEntity getTransferDetailByNo(GetTransferDetailByNoRequest request ) {
return transferBatchService.getTransferDetailByNo(request);
}
/** 通过商家明细单号查询明细单 */
public TransferDetailEntity getTransferDetailByOutNo(GetTransferDetailByOutNoRequest request ) {
return transferBatchService.getTransferDetailByOutNo(request);
}
}

View File

@ -0,0 +1,108 @@
package com.ruoyi.common.pay.wx.util;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.pay.wx.domain.enums.WxNotifyEventType;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.wechat.pay.java.core.notification.Notification;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.model.Transaction;
import javax.servlet.http.HttpServletRequest;
/**
* @author wjh
* 2024/8/12
*/
public class WxPayUtil {
public static final NotificationParser NOTIFICATION_PARSER = SpringUtils.getBean(NotificationParser.class);
/**
* 验签并解析
*/
public static <T> T checkAndParse(HttpServletRequest request, String body, Class<T> clazz) {
// 构造 RequestParam
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(request.getHeader("Wechatpay-Serial"))
.nonce(request.getHeader("Wechatpay-Nonce"))
.signature(request.getHeader("Wechatpay-Signature"))
.timestamp(request.getHeader("Wechatpay-Timestamp"))
.body(body)
.build();
// 验签
return NOTIFICATION_PARSER.parse(requestParam, clazz);
}
/**
* 是否支付成功
*/
public static boolean isSuccess(Transaction transaction) {
return transaction != null && Transaction.TradeStateEnum.SUCCESS.equals(transaction.getTradeState());
}
/**
* 验签并转为业务对象
*/
public static Transaction toTransaction(HttpServletRequest request) {
// 获取原始报文body
String body = HttpUtils.getBody(request);
// 解析通知数据
Notification notification = JSON.parseObject(body, Notification.class);
// 支付成功通知
if (WxNotifyEventType.TRANSACTION_SUCCESS.getValue().equals(notification.getEventType())) {
// 验签解密并转换成 Transaction
return checkAndParse(request, body, Transaction.class);
}
return null;
}
public static String getTransferCLoseReason(String code) {
switch (code) {
case "OVERDUE_CLOSE": return "系统超时关闭,可能原因账户余额不足或其他错误";
case "TRANSFER_SCENE_INVALID": return "付款确认时,转账场景已不可用,系统做关单处理";
default: return "其他关闭原因";
}
}
/**
* 获取转账明细失败原因
*/
public static String getTransferDetailErrorMsg(String code){
switch (code){
case "NAME_NOT_CORRECT":
return "收款人姓名校验不通过,请核实信息";
case "ACCOUNT_FROZEN":
return "该用户账户被冻结";
case "REAL_NAME_CHECK_FAIL":
return "收款人未实名认证,需要用户完成微信实名认证";
case "OPENID_INVALID":
return "微信用户编号格式错误或者不属于商家公众账号";
case "TRANSFER_QUOTA_EXCEED":
return "超过用户单笔收款额度";
case "DAY_RECEIVED_QUOTA_EXCEED":
return "超过用户单日收款额度";
case "MONTH_RECEIVED_QUOTA_EXCEED":
return "超过用户单月收款额度";
case "DAY_RECEIVED_COUNT_EXCEED":
return "超过用户单日收款次数";
case "PRODUCT_AUTH_CHECK_FAIL":
return "未开通该权限或权限被冻结";
case "OVERDUE_CLOSE":
return "超过系统重试期,系统自动关闭";
case "ID_CARD_NOT_CORRECT":
return "收款人身份证校验不通过,请核实信息";
case "ACCOUNT_NOT_EXIST":
return "该用户账户不存在";
case "TRANSFER_RISK":
return "该笔转账可能存在风险,已被微信拦截";
default:
return "其它失败原因";
}
}
}

View File

@ -24,7 +24,7 @@ import com.ruoyi.ss.timeBill.domain.dto.TimeBillPayDTO;
import com.ruoyi.ss.timeBill.domain.enums.TimeBillStatus;
import com.ruoyi.ss.timeBill.service.TimeBillConverter;
import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillPayType;
import com.ruoyi.common.pay.wx.service.WxPayServiceImpl;
import com.ruoyi.common.pay.wx.service.WxPayService;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -66,7 +66,7 @@ public class TimeBillServiceImpl implements TimeBillService
private PayBillService payBillService;
@Autowired
private WxPayServiceImpl wxPayService;
private WxPayService wxPayService;
@Autowired
private PayBillConverter payBillConverter;

View File

@ -2,16 +2,12 @@ package com.ruoyi.ss.transactionBill.domain.vo;
import com.fasterxml.jackson.annotation.JsonView;
import com.ruoyi.common.core.domain.JsonViewProfile;
import com.ruoyi.common.pay.wx.domain.BatchTransferAble;
import com.ruoyi.common.pay.wx.domain.enums.TransferScene;
import com.ruoyi.ss.suit.domain.enums.SuitTimeUnit;
import com.ruoyi.ss.transactionBill.domain.TransactionBill;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
* @author
* 2024/3/4

View File

@ -5,6 +5,7 @@ import com.ruoyi.common.core.redis.RedisLock;
import com.ruoyi.common.core.redis.enums.RedisLockKey;
import com.ruoyi.common.enums.WithdrawServiceType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.pay.wx.util.WxPayUtil;
import com.ruoyi.common.utils.*;
import com.ruoyi.common.utils.collection.CollectionUtils;
import com.ruoyi.ss.account.domain.AccountVO;
@ -39,7 +40,10 @@ import com.ruoyi.ss.transactionBill.domain.enums.*;
import com.ruoyi.ss.transactionBill.mapper.TransactionBillMapper;
import com.ruoyi.ss.transactionBill.service.TransactionBillService;
import com.ruoyi.ss.transactionBill.service.TransactionBillValidator;
import com.ruoyi.ss.transfer.domain.Transfer;
import com.ruoyi.ss.transfer.domain.TransferQuery;
import com.ruoyi.ss.transfer.domain.TransferVO;
import com.ruoyi.ss.transfer.interfaces.AfterTransfer;
import com.ruoyi.ss.transfer.service.TransferConverter;
import com.ruoyi.ss.transfer.service.TransferService;
import com.ruoyi.ss.user.domain.SmUserVo;
@ -73,7 +77,7 @@ import java.util.stream.Collectors;
*/
@Service("transactionBillService")
@Slf4j
public class TransactionBillServiceImpl implements TransactionBillService {
public class TransactionBillServiceImpl implements TransactionBillService, AfterTransfer {
@Autowired
private TransactionBillMapper transactionBillMapper;
@ -708,7 +712,7 @@ public class TransactionBillServiceImpl implements TransactionBillService {
if (TransactionBillPayType.WECHAT.getType().equals(bill.getChannelId())) {
// 微信支付
Transaction transaction = wxPayService.queryOrderByOutTradeNo(billNo);
return wxPayService.isSuccess(transaction);
return WxPayUtil.isSuccess(transaction);
}
// ... TODO 其他支付方式
@ -1138,4 +1142,62 @@ public class TransactionBillServiceImpl implements TransactionBillService {
public int selectSimpleCount(TransactionBillQuery query) {
return transactionBillMapper.selectSimpleCount(query);
}
/**
* 转账成功后
*/
@Override
public int onTransferSuccess(Long bstId) {
if (bstId == null) {
return 0;
}
Integer result = transactionTemplate.execute(status -> {
// 修改提现状态
TransactionBill data = new TransactionBill();
data.setStatus(TransactionBillStatus.WITHDRAW_SUCCESS.getStatus());
TransactionBillQuery query = new TransactionBillQuery();
query.setStatus(TransactionBillStatus.WITHDRAW_PAYING.getStatus());
query.setBillId(bstId);
int update = this.updateByQuery(data, query);
ServiceUtil.assertion(update != 1, "修改提现状态失败,提现状态已发生改变");
return update;
});
return result == null ? 0 : result;
}
/**
* 转账失败后
*/
@Override
public int onTransferFail(Long bstId) {
if (bstId == null) {
return 0;
}
TransactionBillVO withdraw = this.selectWithdrawById(bstId);
if (withdraw == null) {
return 0;
}
Integer result = transactionTemplate.execute(status -> {
// 修改提现状态
TransactionBill data = new TransactionBill();
data.setStatus(TransactionBillStatus.WITHDRAW_FAIL.getStatus());
TransactionBillQuery query = new TransactionBillQuery();
query.setStatus(TransactionBillStatus.WITHDRAW_PAYING.getStatus());
query.setBillId(bstId);
int update = this.updateByQuery(data, query);
ServiceUtil.assertion(update != 1, "修改提现状态失败,提现状态已发生改变");
// 将提现金额退回用户
userService.addBalance(withdraw.getUserId(), withdraw.getMoney(), String.format("提现%s打款失败", withdraw.getBillNo()), RecordBalanceBstType.WITHDRAW, bstId);
return update;
});
return result == null ? 0 : result;
}
}

View File

@ -53,4 +53,7 @@ public class Transfer extends BaseEntity
@ApiModelProperty("批次总金额(元)")
private BigDecimal totalAmount;
@ApiModelProperty("关闭原因")
private String closeReason;
}

View File

@ -1,5 +1,7 @@
package com.ruoyi.ss.transfer.domain.enums;
import com.ruoyi.ss.transactionBill.service.impl.TransactionBillServiceImpl;
import com.ruoyi.ss.transfer.interfaces.AfterTransfer;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -12,9 +14,19 @@ import lombok.Getter;
@AllArgsConstructor
public enum TransferBstType {
WITHDRAW("1", "提现");
WITHDRAW("1", "提现", TransactionBillServiceImpl.class);
private final String type;
private final String name;
private final Class<? extends AfterTransfer> afterTransfer;
public static TransferBstType parse(String type) {
for (TransferBstType value : TransferBstType.values()) {
if (value.getType().equals(type)) {
return value;
}
}
return null;
}
}

View File

@ -11,10 +11,11 @@ import lombok.Getter;
@AllArgsConstructor
public enum TransferStatus {
TRANSFER_ING("1", "转账中"),
TRANSFER_SUCCESS("2", "已转账"),
TRANSFER_PART_SUCCESS("3", "部分成功"),
TRANSFER_FAIL("4", "转账失败");
WAIT_TRANSFER("1", "待转账"),
TRANSFER_ING("2", "转账中"),
TRANSFER_SUCCESS("3", "已转账"),
TRANSFER_PART_SUCCESS("4", "部分成功"),
TRANSFER_FAIL("5", "转账失败");
private final String status;
private final String msg;

View File

@ -0,0 +1,16 @@
package com.ruoyi.ss.transfer.interfaces;
/**
* 转账结束后处理
* @author wjh
* 2024/8/12
*/
public interface AfterTransfer {
int onTransferSuccess(Long bstId);
default int onTransferPartSuccess(Long bstId) {return 1;}
int onTransferFail(Long bstId);
}

View File

@ -66,4 +66,9 @@ public interface TransferMapper
* 批次单号查询
*/
TransferVO selectByBatchNo(String batchNo);
/**
* 条件修改
*/
int updateByQuery(@Param("data") Transfer data, @Param("query") TransferQuery query);
}

View File

@ -17,7 +17,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
st.batch_name,
st.batch_remark,
st.create_time,
st.total_amount
st.total_amount,
st.close_reason
from ss_transfer st
</sql>
@ -64,6 +65,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="batchRemark != null and batchRemark != ''">batch_remark,</if>
<if test="createTime != null">create_time,</if>
<if test="totalAmount != null">total_amount,</if>
<if test="closeReason != null and closeReason != ''">close_reason,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="batchNo != null and batchNo != ''">#{batchNo},</if>
@ -75,25 +77,41 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="batchRemark != null and batchRemark != ''">#{batchRemark},</if>
<if test="createTime != null">#{createTime},</if>
<if test="totalAmount != null">#{totalAmount},</if>
<if test="closeReason != null and closeReason != ''">#{closeReason},</if>
</trim>
</insert>
<update id="updateTransfer" parameterType="Transfer">
update ss_transfer
<trim prefix="SET" suffixOverrides=",">
<if test="data.batchNo != null and data.batchNo != ''">batch_no = #{data.batchNo},</if>
<if test="data.accountType != null and data.accountType != ''">account_type = #{data.accountType},</if>
<if test="data.bstType != null and data.bstType != ''">bst_type = #{data.bstType},</if>
<if test="data.bstId != null">bst_id = #{data.bstId},</if>
<if test="data.status != null and data.status != ''">`status` = #{data.status},</if>
<if test="data.batchName != null and data.batchName != ''">batch_name = #{data.batchName},</if>
<if test="data.batchRemark != null and data.batchRemark != ''">batch_remark = #{data.batchRemark},</if>
<if test="data.createTime != null">create_time = #{data.createTime},</if>
<if test="data.totalAmount != null">total_amount = #{data.totalAmount},</if>
<include refid="updateColumns"/>
</trim>
where batch_id = #{data.batchId}
</update>
<sql id="updateColumns">
<if test="data.batchNo != null and data.batchNo != ''">batch_no = #{data.batchNo},</if>
<if test="data.accountType != null and data.accountType != ''">account_type = #{data.accountType},</if>
<if test="data.bstType != null and data.bstType != ''">bst_type = #{data.bstType},</if>
<if test="data.bstId != null">bst_id = #{data.bstId},</if>
<if test="data.status != null and data.status != ''">`status` = #{data.status},</if>
<if test="data.batchName != null and data.batchName != ''">batch_name = #{data.batchName},</if>
<if test="data.batchRemark != null and data.batchRemark != ''">batch_remark = #{data.batchRemark},</if>
<if test="data.createTime != null">create_time = #{data.createTime},</if>
<if test="data.totalAmount != null">total_amount = #{data.totalAmount},</if>
<if test="data.closeReason != null and data.closeReason != ''">close_reason = #{data.closeReason},</if>
</sql>
<update id="updateByQuery">
update ss_transfer st
<trim prefix="SET" suffixOverrides=",">
<include refid="updateColumns"/>
</trim>
<where>
<include refid="searchCondition"/>
</where>
</update>
<delete id="deleteTransferByBatchId" parameterType="Long">
delete from ss_transfer where batch_id = #{batchId}
</delete>

View File

@ -0,0 +1,18 @@
package com.ruoyi.ss.transfer.service;
import com.ruoyi.ss.transfer.domain.TransferVO;
import java.util.List;
/**
* @author wjh
* 2024/8/12
*/
public interface TransferAssembler {
/**
* 拼接明细
*/
void assembleDetail(List<TransferVO> list);
}

View File

@ -78,4 +78,19 @@ public interface TransferService
* @param batchNo
*/
TransferVO selectByBatchNo(String batchNo);
/**
* 根据查询条件更新
*/
int updateByQuery(Transfer data, TransferQuery query);
/**
* 刷新转账单状态
*/
int refreshTransferStatus(TransferVO transfer);
/**
* 刷新转账状态
*/
int refreshTransferStatus(Long batchId);
}

View File

@ -0,0 +1,51 @@
package com.ruoyi.ss.transfer.service.impl;
import com.ruoyi.common.utils.collection.CollectionUtils;
import com.ruoyi.ss.transfer.domain.TransferVO;
import com.ruoyi.ss.transfer.service.TransferAssembler;
import com.ruoyi.ss.transferDetail.domain.TransferDetailQuery;
import com.ruoyi.ss.transferDetail.domain.TransferDetailVO;
import com.ruoyi.ss.transferDetail.service.ITransferDetailService;
import com.ruoyi.ss.transferDetail.service.impl.TransferDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author wjh
* 2024/8/12
*/
@Service
public class TransferAssemblerImpl implements TransferAssembler {
@Autowired
private ITransferDetailService transferDetailService;
/**
* 拼接明细
*/
@Override
public void assembleDetail(List<TransferVO> list) {
if (CollectionUtils.isEmptyElement(list)) {
return;
}
TransferDetailQuery query = new TransferDetailQuery();
query.setTransferIds(CollectionUtils.map(list, TransferVO::getBatchId));
Map<Long, List<TransferDetailVO>> group = transferDetailService.selectTransferDetailList(query).stream()
.collect(Collectors.groupingBy(TransferDetailVO::getTransferId));
for (TransferVO transfer : list) {
List<TransferDetailVO> detailList = group.get(transfer.getBatchId());
if (CollectionUtils.isNotEmptyElement(detailList)) {
transfer.setDetailList(detailList);
} else {
transfer.setDetailList(Collections.emptyList());
}
}
}
}

View File

@ -52,7 +52,7 @@ public class TransferConverterImpl implements TransferConverter {
}
vo.setBstType(TransferBstType.WITHDRAW.getType());
vo.setBstId(bill.getBillId());
vo.setStatus(TransferStatus.TRANSFER_ING.getStatus());
vo.setStatus(TransferStatus.WAIT_TRANSFER.getStatus());
vo.setBatchName("提现转账");
vo.setBatchRemark(String.format("提现申请%s转账", bill.getBillNo()));
vo.setTotalAmount(bill.getArrivalAmount());
@ -60,9 +60,10 @@ public class TransferConverterImpl implements TransferConverter {
// 生成明细
List<TransferDetailVO> detailList = new ArrayList<>();
TransferDetailVO detail = new TransferDetailVO();
detail.setStatus(TransferDetailStatus.TRANSFER_ING.getStatus());
detail.setStatus(TransferDetailStatus.WAIT_TRANSFER.getStatus());
detail.setAmount(bill.getArrivalAmount());
detail.setAccountNo(bill.getAccountNo());
detail.setRemark(String.format("提现申请%s转账", bill.getBillNo()));
detailList.add(detail);
vo.setDetailList(detailList);

View File

@ -1,12 +1,34 @@
package com.ruoyi.ss.transfer.service.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.pay.wx.domain.enums.WxTransferBatchStatus;
import com.ruoyi.common.pay.wx.domain.enums.WxTransferDetailStatus;
import com.ruoyi.common.pay.wx.service.WxPayService;
import com.ruoyi.common.pay.wx.service.WxTransferService;
import com.ruoyi.common.pay.wx.util.WxPayUtil;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.common.utils.SnowFlakeUtil;
import com.ruoyi.common.utils.collection.CollectionUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.ss.account.domain.enums.AccountType;
import com.ruoyi.ss.transfer.domain.enums.TransferBstType;
import com.ruoyi.ss.transfer.domain.enums.TransferStatus;
import com.ruoyi.ss.transfer.interfaces.AfterTransfer;
import com.ruoyi.ss.transfer.service.TransferAssembler;
import com.ruoyi.ss.transferDetail.domain.TransferDetail;
import com.ruoyi.ss.transferDetail.domain.TransferDetailQuery;
import com.ruoyi.ss.transferDetail.domain.TransferDetailVO;
import com.ruoyi.ss.transferDetail.domain.enums.TransferDetailStatus;
import com.ruoyi.ss.transferDetail.service.ITransferDetailService;
import com.wechat.pay.java.service.transferbatch.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.ss.transfer.mapper.TransferMapper;
@ -23,6 +45,7 @@ import org.springframework.transaction.support.TransactionTemplate;
* @date 2024-08-09
*/
@Service
@Slf4j
public class TransferServiceImpl implements TransferService
{
@Autowired
@ -34,6 +57,15 @@ public class TransferServiceImpl implements TransferService
@Autowired
private ITransferDetailService transferDetailService;
@Autowired
private WxPayService wxPayService;
@Autowired
private TransferAssembler transferAssembler;
@Autowired
private WxTransferService wxTransferService;
/**
* 查询转账
*
@ -131,6 +163,7 @@ public class TransferServiceImpl implements TransferService
@Override
public boolean requestTransfer(String batchNo) {
TransferVO transfer = this.selectByBatchNo(batchNo);
transferAssembler.assembleDetail(Collections.singletonList(transfer));
return this.requestTransfer(transfer);
}
@ -138,23 +171,208 @@ public class TransferServiceImpl implements TransferService
* 请求转账
*/
private boolean requestTransfer(TransferVO transfer) {
if (transfer == null) {
if (transfer == null || transfer.getBatchId() == null) {
return false;
}
// TODO 请求转账
transactionTemplate.execute(status -> {
// 请求转账
Boolean result = transactionTemplate.execute(status -> {
// 修改状态
Transfer data = new Transfer();
data.setStatus(TransferStatus.TRANSFER_ING.getStatus());
TransferQuery query = new TransferQuery();
query.setBatchId(transfer.getBatchId());
query.setStatus(TransferStatus.WAIT_TRANSFER.getStatus());
int update = this.updateByQuery(data, query);
ServiceUtil.assertion(update != 1, "更新转账状态失败");
// TODO 修改状态
// 更新子表状态
TransferDetail detailData = new TransferDetail();
detailData.setStatus(TransferDetailStatus.TRANSFER_ING.getStatus());
TransferDetailQuery detailQuery = new TransferDetailQuery();
detailQuery.setTransferId(transfer.getBatchId());
detailQuery.setStatus(TransferDetailStatus.WAIT_TRANSFER.getStatus());
int updateDetail = transferDetailService.updateByQuery(detailData, detailQuery);
ServiceUtil.assertion(updateDetail != 1, "更新转账明细状态失败");
// 发起转账
if (AccountType.WECHAT.getType().equals(transfer.getAccountType())) {
wxTransferService.batchTransfer(transfer);
} else {
throw new ServiceException("暂不支持该方式进行线上转账");
}
// TODO 发起转账
return true;
});
return result != null && result;
}
@Override
public TransferVO selectByBatchNo(String batchNo) {
return transferMapper.selectByBatchNo(batchNo);
}
@Override
public int updateByQuery(Transfer data, TransferQuery query) {
return transferMapper.updateByQuery(data, query);
}
@Override
public int refreshTransferStatus(TransferVO transfer) {
if (transfer == null) {
return 0;
}
// 微信
if (AccountType.WECHAT.getType().equals(transfer.getAccountType())) {
return this.refreshTransferStatusWx(transfer);
} else {
throw new ServiceException("该付款方式不支持刷新操作");
}
}
@Override
public int refreshTransferStatus(Long batchId) {
TransferVO transfer = selectTransferByBatchId(batchId);
transferAssembler.assembleDetail(Collections.singletonList(transfer));
return this.refreshTransferStatus(transfer);
}
private int refreshTransferStatusWx(TransferVO transfer) {
if (transfer == null) {
return 0;
}
// 主表信息
GetTransferBatchByOutNoRequest request = new GetTransferBatchByOutNoRequest();
request.setOutBatchNo(transfer.getBatchNo());
request.setNeedQueryDetail(false);
TransferBatchEntity transferBatchEntity = wxTransferService.getTransferBatchByOutNo(request);
if (transferBatchEntity == null || transferBatchEntity.getTransferBatch() == null) {
return 0;
}
// 批次信息
TransferBatchGet transferBatch = transferBatchEntity.getTransferBatch();
String batchStatus = transferBatch.getBatchStatus();
// 仅处理完成和关单的状态
log.info("【刷新微信转账状态】批次信息:{}", transferBatch);
if (!WxTransferBatchStatus.asList(WxTransferBatchStatus.FINISHED, WxTransferBatchStatus.CLOSED).contains(batchStatus)) {
return 1;
}
// 待更新的数据
Transfer data = new Transfer();
List<TransferDetail> detaiList = new ArrayList<>();
// 已完成所有明细都处理完成了
if (WxTransferBatchStatus.FINISHED.getStatus().equals(transferBatch.getBatchStatus())) {
if (transferBatch.getSuccessNum() != null && Objects.equals(transferBatch.getSuccessNum(), transferBatch.getTotalNum())) {
// 全部成功
data.setStatus(TransferStatus.TRANSFER_SUCCESS.getStatus());
} else if (transferBatch.getFailNum() != null && Objects.equals(transferBatch.getFailNum(), transferBatch.getTotalNum())) {
// 全部失败
data.setStatus(TransferStatus.TRANSFER_FAIL.getStatus());
} else {
// 部分成功
data.setStatus(TransferStatus.TRANSFER_PART_SUCCESS.getStatus());
}
// 处理明细信息
detaiList.addAll(this.buildRefreshDetailList(transfer.getDetailList()));
}
// 关单失败并给出关单原因
else if (WxTransferBatchStatus.CLOSED.getStatus().equals(transferBatch.getBatchStatus())) {
data.setStatus(TransferStatus.TRANSFER_FAIL.getStatus());
data.setCloseReason(WxPayUtil.getTransferCLoseReason(transferBatch.getCloseReason().name()));
// 所有明细都修改为失败
for (TransferDetailVO detail : transfer.getDetailList()) {
TransferDetail failDetail = new TransferDetail();
failDetail.setDetailId(detail.getDetailId());
failDetail.setStatus(TransferDetailStatus.TRANSFER_FAIL.getStatus());
detaiList.add(failDetail);
}
}
// 保存到数据库
Integer result = transactionTemplate.execute(status -> {
// 修改主表信息
TransferQuery query = new TransferQuery();
query.setStatus(TransferStatus.TRANSFER_ING.getStatus());
query.setBatchId(transfer.getBatchId());
int update = this.updateByQuery(data, query);
ServiceUtil.assertion(update != 1, "修改转账批次失败,状态已发生变化");
// 修改明细信息
for (TransferDetail detail : detaiList) {
TransferDetailQuery detailQuery = new TransferDetailQuery();
detailQuery.setDetailId(detail.getDetailId());
detailQuery.setStatus(TransferStatus.TRANSFER_ING.getStatus());
int detailUpdate = transferDetailService.updateByQuery(detail, detailQuery);
ServiceUtil.assertion(detailUpdate != 1, "转账明细修改失败,状态已发生变化");
}
// 修改业务信息
TransferBstType bstType = TransferBstType.parse(transfer.getBstType());
ServiceUtil.assertion(bstType == null, "业务类型不存在");
ServiceUtil.assertion(bstType.getAfterTransfer() == null, "业务处理器不存在");
AfterTransfer afterTransfer = SpringUtils.getBean(bstType.getAfterTransfer());
// 成功
int bstResult = 0;
if (TransferStatus.TRANSFER_SUCCESS.getStatus().equals(data.getStatus())) {
bstResult = afterTransfer.onTransferSuccess(transfer.getBstId());
}
// 部分成功
else if (TransferStatus.TRANSFER_PART_SUCCESS.getStatus().equals(data.getStatus())) {
bstResult = afterTransfer.onTransferPartSuccess(transfer.getBstId());
}
// 失败
else if (TransferStatus.TRANSFER_FAIL.getStatus().equals(data.getStatus())) {
bstResult = afterTransfer.onTransferFail(transfer.getBstId());
}
ServiceUtil.assertion(bstResult == 0, "业务执行失败");
return update;
});
return result == null ? 0 : result;
}
private List<TransferDetail> buildRefreshDetailList(List<TransferDetailVO> detailList) {
List<TransferDetail> updateList = new ArrayList<>();
if (CollectionUtils.isEmptyElement(detailList)) {
return updateList;
}
for (TransferDetailVO detail : detailList) {
GetTransferDetailByOutNoRequest detailRequest = new GetTransferDetailByOutNoRequest();
detailRequest.setOutBatchNo(detail.getTransferBatchNo());
detailRequest.setOutDetailNo(detail.getDetailNo());
TransferDetailEntity transferDetailEntity = wxTransferService.getTransferDetailByOutNo(detailRequest);
log.info("【刷新微信转账状态】明细批次信息:{}", transferDetailEntity);
if (transferDetailEntity == null) {
continue;
}
TransferDetail updateDetail = new TransferDetail();
updateDetail.setDetailId(detail.getDetailId());
// 转账成功
if (WxTransferDetailStatus.SUCCESS.getStatus().equals(transferDetailEntity.getDetailStatus())) {
updateDetail.setStatus(TransferDetailStatus.TRANSFER_SUCCESS.getStatus());
updateList.add(updateDetail);
}
// 转账失败
else if (WxTransferDetailStatus.FAIL.getStatus().equals(transferDetailEntity.getDetailStatus())){
updateDetail.setStatus(TransferDetailStatus.TRANSFER_FAIL.getStatus());
updateDetail.setFailReason(WxPayUtil.getTransferDetailErrorMsg(transferDetailEntity.getFailReason().name()));
updateList.add(updateDetail);
}
}
return updateList;
}
}

View File

@ -45,4 +45,8 @@ public class TransferDetail extends BaseEntity
@ApiModelProperty("收款用户姓名")
private String userName;
@Excel(name = "收款用户姓名")
@ApiModelProperty("失败原因")
private String failReason;
}

View File

@ -1,11 +1,18 @@
package com.ruoyi.ss.transferDetail.domain;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* @author wjh
* 2024/8/9
*/
@Data
public class TransferDetailQuery extends TransferDetailVO{
@ApiModelProperty("批次ID列表")
private List<Long> transferIds;
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.ss.transferDetail.domain;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
@ -8,4 +9,8 @@ import lombok.Data;
*/
@Data
public class TransferDetailVO extends TransferDetail{
@ApiModelProperty("批次单号")
private String transferBatchNo;
}

View File

@ -12,9 +12,10 @@ import lombok.Getter;
@AllArgsConstructor
public enum TransferDetailStatus {
TRANSFER_ING("1", "转账中"),
TRANSFER_SUCCESS("2", "已转账"),
TRANSFER_FAIL("3", "转账失败");
WAIT_TRANSFER("1", "待转账"),
TRANSFER_ING("2", "转账中"),
TRANSFER_SUCCESS("3", "已转账"),
TRANSFER_FAIL("4", "转账失败");
private final String status;
private final String msg;

View File

@ -67,4 +67,8 @@ public interface TransferDetailMapper
*/
int batchInsert(@Param("list") List<TransferDetailVO> list);
/**
* 条件更新
*/
int updateByQuery(@Param("data") TransferDetail data, @Param("query") TransferDetailQuery query);
}

View File

@ -16,18 +16,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
std.remark,
std.account_no,
std.user_name,
std.create_time
std.create_time,
std.fail_reason,
st.batch_no as transfer_batch_no
from ss_transfer_detail std
left join ss_transfer st on st.batch_id = std.transfer_id
</sql>
<sql id="searchCondition">
<if test="query.detailId != null "> and std.detail_id = #{query.detailId}</if>
<if test="query.detailNo != null and query.detailNo != ''"> and std.detail_no like concat('%', #{query.detailNo}, '%')</if>
<if test="query.transferId != null "> and std.transfer_id = #{query.transferId}</if>
<if test="query.transferBatchNo != null and query.transferBatchNo != '' "> and st.batch_no like concat('%', #{query.transferBatchNo}, '%')</if>
<if test="query.status != null and query.status != ''"> and std.status = #{query.status}</if>
<if test="query.remark != null and query.remark != ''"> and std.remark like concat('%', #{query.remark}, '%')</if>
<if test="query.accountNo != null and query.accountNo != ''"> and std.account_no like concat('%', #{query.accountNo}, '%')</if>
<if test="query.userName != null and query.userName != ''"> and std.user_name like concat('%', #{query.userName}, '%')</if>
<if test="query.transferIds != null and query.transferIds.size() > 0">
and std.transfer_id in
<foreach collection="query.transferIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
</sql>
<select id="selectTransferDetailList" parameterType="TransferDetailQuery" resultMap="TransferDetailResult">
@ -53,6 +63,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="accountNo != null and accountNo != ''">account_no,</if>
<if test="userName != null">user_name,</if>
<if test="createTime != null">create_time,</if>
<if test="failReason != null">fail_reason,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="detailNo != null and detailNo != ''">#{detailNo},</if>
@ -63,6 +74,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="accountNo != null and accountNo != ''">#{accountNo},</if>
<if test="userName != null">#{userName},</if>
<if test="createTime != null">#{createTime},</if>
<if test="failReason != null">#{failReason},</if>
</trim>
</insert>
@ -76,7 +88,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
remark,
account_no,
user_name,
create_time
create_time,
fail_reason
)
values
<foreach collection="list" item="i" separator=",">
@ -97,6 +110,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="i.userName == null">default,</if>
<if test="i.createTime != null">#{i.createTime},</if>
<if test="i.createTime == null">default,</if>
<if test="i.failReason != null and i.failReason != ''">#{i.failReason},</if>
<if test="i.failReason == null or i.failReason == ''">default,</if>
</trim>
</foreach>
</insert>
@ -104,18 +119,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<update id="updateTransferDetail" parameterType="TransferDetail">
update ss_transfer_detail
<trim prefix="SET" suffixOverrides=",">
<if test="data.detailNo != null and data.detailNo != ''">detail_no = #{data.detailNo},</if>
<if test="data.transferId != null">transfer_id = #{data.transferId},</if>
<if test="data.status != null and data.status != ''">`status` = #{data.status},</if>
<if test="data.amount != null">amount = #{data.amount},</if>
<if test="data.remark != null and data.remark != ''">remark = #{data.remark},</if>
<if test="data.accountNo != null and data.accountNo != ''">account_no = #{data.accountNo},</if>
<if test="data.userName != null">user_name = #{data.userName},</if>
<if test="data.createTime != null">create_time = #{data.createTime},</if>
<include refid="updateColumns"/>
</trim>
where detail_id = #{data.detailId}
</update>
<sql id="updateColumns">
<if test="data.detailNo != null and data.detailNo != ''">detail_no = #{data.detailNo},</if>
<if test="data.transferId != null">transfer_id = #{data.transferId},</if>
<if test="data.status != null and data.status != ''">`status` = #{data.status},</if>
<if test="data.amount != null">amount = #{data.amount},</if>
<if test="data.remark != null and data.remark != ''">remark = #{data.remark},</if>
<if test="data.accountNo != null and data.accountNo != ''">account_no = #{data.accountNo},</if>
<if test="data.userName != null">user_name = #{data.userName},</if>
<if test="data.createTime != null">create_time = #{data.createTime},</if>
<if test="data.failReason != null and data.failReason != ''">fail_reason = #{data.failReason},</if>
</sql>
<update id="updateByQuery">
update ss_transfer_detail std
<trim prefix="SET" suffixOverrides=",">
<include refid="updateColumns"/>
</trim>
<where>
<include refid="searchCondition"/>
</where>
</update>
<delete id="deleteTransferDetailByDetailId" parameterType="Long">
delete from ss_transfer_detail where detail_id = #{detailId}
</delete>

View File

@ -67,4 +67,9 @@ public interface ITransferDetailService
*/
int batchInsert(List<TransferDetailVO> list);
/**
* 条件更新
*/
int updateByQuery(TransferDetail data, TransferDetailQuery query);
}

View File

@ -108,6 +108,11 @@ public class TransferDetailServiceImpl implements ITransferDetailService
return transferDetailMapper.batchInsert(list);
}
@Override
public int updateByQuery(TransferDetail data, TransferDetailQuery query) {
return transferDetailMapper.updateByQuery(data, query);
}
private void setBaseInfo(TransferDetail detail) {
detail.setDetailNo(String.valueOf(SnowFlakeUtil.newId()));
detail.setCreateTime(DateUtils.getNowDate());

View File

@ -0,0 +1,52 @@
package com.ruoyi.task.transfer;
import com.ruoyi.ss.transfer.domain.TransferQuery;
import com.ruoyi.ss.transfer.domain.TransferVO;
import com.ruoyi.ss.transfer.domain.enums.TransferStatus;
import com.ruoyi.ss.transfer.service.TransferAssembler;
import com.ruoyi.ss.transfer.service.TransferService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author wjh
* 2024/8/12
*/
@Component
@Slf4j
public class TransferTask {
@Autowired
private TransferService transferService;
@Autowired
private TransferAssembler transferAssembler;
/**
* 定时刷新转账状态
*/
public void refreshTransferStatus() {
// 查询转账中的转账单
TransferQuery query = new TransferQuery();
query.setStatus(TransferStatus.TRANSFER_ING.getStatus());
List<TransferVO> transferList = transferService.selectTransferList(query);
// 拼接明细
transferAssembler.assembleDetail(transferList);
// 执行刷新
for (TransferVO transfer : transferList) {
try {
transferService.refreshTransferStatus(transfer);
} catch (Exception e) {
log.error("刷新{}转账状态失败:{}", transfer.getBatchId(), e.getMessage());
}
}
}
}

View File

@ -4,7 +4,9 @@ import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.pay.wx.domain.NotifyEventType;
import com.ruoyi.common.pay.wx.domain.enums.WxNotifyEventType;
import com.ruoyi.common.pay.wx.domain.enums.WxTransferBatchStatus;
import com.ruoyi.common.pay.wx.util.WxPayUtil;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.ss.payBill.service.PayBillService;
@ -12,12 +14,12 @@ import com.ruoyi.ss.refund.service.RefundService;
import com.ruoyi.ss.transactionBill.domain.TransactionBill;
import com.ruoyi.ss.transactionBill.service.TransactionBillService;
import com.ruoyi.common.pay.wx.domain.enums.AttachEnums;
import com.ruoyi.common.pay.wx.service.WxPayService;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.notification.Notification;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.model.RefundNotification;
import com.wechat.pay.java.service.refund.model.Status;
import com.wechat.pay.java.service.transferbatch.model.TransferBatchGet;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
@ -40,9 +42,6 @@ import java.util.Date;
@Slf4j
public class AppPayController extends BaseController {
@Autowired
private WxPayService wxPayService;
@Autowired
private TransactionBillService transactionBillService;
@ -69,11 +68,11 @@ public class AppPayController extends BaseController {
public ResponseEntity<Boolean> wxPayNotify(HttpServletRequest request) {
try {
log.info("收到微信支付回调{}", request);
// 转为微信支付业务结果对象
Transaction transaction = wxPayService.toTransaction(request);
Transaction transaction = WxPayUtil.toTransaction(request);
// 支付成功
if (wxPayService.isSuccess(transaction)) {
if (WxPayUtil.isSuccess(transaction)) {
// 根据附加信息判断是哪个订单
String attach = transaction.getAttach();
if (AttachEnums.TRANSACTION_BILL.getAttach().equals(attach)) {
@ -103,9 +102,9 @@ public class AppPayController extends BaseController {
Notification notification = JSON.parseObject(body, Notification.class);
log.info("【微信退款回调】转换成notification: " + JSON.toJSONString(notification));
// 退款成功通知
if (NotifyEventType.REFUND_SUCCESS.getValue().equals(notification.getEventType())) {
if (WxNotifyEventType.REFUND_SUCCESS.getValue().equals(notification.getEventType())) {
// 验签解密并转换成 RefundNotification
RefundNotification refundNotification = wxPayService.checkAndParse(request, body, RefundNotification.class);
RefundNotification refundNotification = WxPayUtil.checkAndParse(request, body, RefundNotification.class);
log.info("【微信退款回调】转换成RefundNotification: " + JSON.toJSONString(refundNotification));
if (Status.SUCCESS.equals(refundNotification.getRefundStatus())) {
// 退款成功操作
@ -127,11 +126,27 @@ public class AppPayController extends BaseController {
return AjaxResult.success(transactionBillService.getPayResult(billNo));
}
@ApiOperation("微信商户支付通知")
@ApiOperation("微信转账到零钱回调")
@PostMapping("/notify/wx/transfer")
@Anonymous
public ResponseEntity<Boolean> wxTransferNotify(HttpServletRequest request) {
try {
wxPayService.wxTransferNotify(request);
log.info("收到微信转账到零钱回调{}", request);
String body = HttpUtils.getBody(request);
// 解析通知数据
Notification notification = JSON.parseObject(body, Notification.class);
// 商户转账成功通知
if (WxNotifyEventType.MCHTRANSFER_BATCH_FINISHED.getValue().equals(notification.getEventType())) {
// 验签解密并转换成 TransferBatchGet
TransferBatchGet transferBatchGet = WxPayUtil.checkAndParse(request, body, TransferBatchGet.class);
// 修改订单状态
if (WxTransferBatchStatus.FINISHED.getStatus().equals(transferBatchGet.getBatchStatus())) {
// TODO 提现成功
} else if (WxTransferBatchStatus.CLOSED.getStatus().equals(transferBatchGet.getBatchStatus())) {
// TODO 提现失败
}
}
} catch (ValidationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
} catch (Exception e) {

View File

@ -73,35 +73,13 @@ public class TransferController extends BaseController
}
/**
* 转账
* 新转账结果
*/
@PreAuthorize("@ss.hasPermi('ss:transfer:add')")
@Log(title = "转账", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody Transfer transfer)
@PreAuthorize("@ss.hasPermi('ss:transfer:refresh')")
@PutMapping(value = "/{batchId}/refresh")
public AjaxResult refreshStatus(@PathVariable("batchId") Long batchId)
{
return toAjax(transferService.insertTransfer(transfer));
return toAjax(transferService.refreshTransferStatus(batchId));
}
/**
* 修改转账
*/
@PreAuthorize("@ss.hasPermi('ss:transfer:edit')")
@Log(title = "转账", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody Transfer transfer)
{
return toAjax(transferService.updateTransfer(transfer));
}
/**
* 删除转账
*/
@PreAuthorize("@ss.hasPermi('ss:transfer:remove')")
@Log(title = "转账", businessType = BusinessType.DELETE)
@DeleteMapping("/{batchIds}")
public AjaxResult remove(@PathVariable Long[] batchIds)
{
return toAjax(transferService.deleteTransferByBatchIds(batchIds));
}
}

View File

@ -20,9 +20,11 @@ wx:
# apiV3密钥
apiV3Key: 49819e0f0abdb2df3246f7b27f264d75
# 通知回调地址
notifyUrl: http://124.221.246.124:2290/app/pay/notify/wx # 内网穿透
notifyUrl: http://124.221.246.124:2290/app/pay/notify/wx
# 退款通知回调地址
refundNotifyUrl: http://124.221.246.124:2290/app/pay/notify/wx/refund
# 转账回调地址
transferNotifyUrl: https://kg-dev.chuangtewl.com/dev-api/app/pay/notify/wx/transfer
# 密钥所在位置
privateKeyPath: D:/project/证书/wxpay-kg/apiclient_key.pem
# 证书序列号

View File

@ -20,9 +20,11 @@ wx:
# apiV3密钥
apiV3Key: 49819e0f0abdb2df3246f7b27f264d75
# 通知回调地址
notifyUrl: https://kg.chuangtewl.com/prod-api/app/pay/notify/wx # 正式环境
notifyUrl: https://kg.chuangtewl.com/prod-api/app/pay/notify/wx
# 退款通知回调地址
refundNotifyUrl: https://kg.chuangtewl.com/prod-api/app/pay/notify/wx/refund
# 转账回调地址
transferNotifyUrl: https://kg.chuangtewl.com/prod-api/app/pay/notify/wx/transfer
# 密钥所在位置
privateKeyPath: /www/wwwroot/smart-switch/wxpay/apiclient_key.pem
# 证书序列号