feat(withdraw): 实现分销商提现微信自动转账功能
- 新增 WxTransferService 服务类实现微信商家转账到零钱功能 - 在 ShopDealerWithdrawController 中集成微信转账服务 - 修改 update 方法支持微信收款方式的自动转账处理 - 添加事务管理确保转账操作的数据一致性 - 实现转账参数验证和错误处理机制 - 支持通过 openid 自动获取和用户姓名验证 - 添加转账金额转换和批次号生成逻辑
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
package com.gxwebsoft.payment.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.gxwebsoft.common.system.entity.Payment;
|
||||
import com.gxwebsoft.payment.exception.PaymentException;
|
||||
import com.wechat.pay.java.core.Config;
|
||||
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.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 微信支付-商家转账到零钱(批量转账)封装
|
||||
* 使用 wechatpay-java (APIv3) SDK 发起转账。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class WxTransferService {
|
||||
|
||||
@Resource
|
||||
private WxPayConfigService wxPayConfigService;
|
||||
|
||||
/**
|
||||
* 发起单笔“商家转账到零钱”(使用批量转账接口,明细数=1)。
|
||||
*
|
||||
* @param tenantId 租户ID(用于获取微信支付配置)
|
||||
* @param openid 收款用户openid(必须是该appid下的openid)
|
||||
* @param amountYuan 转账金额(单位:元)
|
||||
* @param outBatchNo 商家批次单号(字母/数字,商户内唯一)
|
||||
* @param outDetailNo 商家明细单号(字母/数字,批次内唯一)
|
||||
* @param batchName 批次名称(<=32字符)
|
||||
* @param remark 备注(<=32字符),既用于批次备注,也用于单条明细备注
|
||||
* @param userName 收款用户姓名(可选;金额>=2000元时强制要求)
|
||||
*/
|
||||
public InitiateBatchTransferResponse initiateSingleTransfer(Integer tenantId,
|
||||
String openid,
|
||||
BigDecimal amountYuan,
|
||||
String outBatchNo,
|
||||
String outDetailNo,
|
||||
String batchName,
|
||||
String remark,
|
||||
String userName) throws PaymentException {
|
||||
|
||||
if (tenantId == null) {
|
||||
throw PaymentException.paramError("租户ID不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(openid)) {
|
||||
throw PaymentException.paramError("收款用户openid不能为空");
|
||||
}
|
||||
if (amountYuan == null || amountYuan.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw PaymentException.amountError("转账金额必须大于0");
|
||||
}
|
||||
if (StrUtil.isBlank(outBatchNo) || !outBatchNo.matches("^[0-9A-Za-z]+$")) {
|
||||
throw PaymentException.paramError("outBatchNo不合法(仅允许数字/大小写字母)");
|
||||
}
|
||||
if (StrUtil.isBlank(outDetailNo) || !outDetailNo.matches("^[0-9A-Za-z]+$")) {
|
||||
throw PaymentException.paramError("outDetailNo不合法(仅允许数字/大小写字母)");
|
||||
}
|
||||
|
||||
// 微信要求金额单位为“分”,必须为整数
|
||||
long amountFen = amountYuan
|
||||
.movePointRight(2)
|
||||
.setScale(0, RoundingMode.HALF_UP)
|
||||
.longValueExact();
|
||||
|
||||
if (amountFen <= 0) {
|
||||
throw PaymentException.amountError("转账金额换算为分后必须大于0");
|
||||
}
|
||||
|
||||
// 金额 >= 2000 元时,必须传收款用户姓名;金额 < 0.3 元时,不允许传姓名
|
||||
boolean needUserName = amountFen >= 2000L * 100L;
|
||||
boolean forbidUserName = amountFen < 30L;
|
||||
if (needUserName && StrUtil.isBlank(userName)) {
|
||||
throw PaymentException.paramError("转账金额>=2000元时必须提供收款用户姓名");
|
||||
}
|
||||
if (forbidUserName) {
|
||||
userName = null;
|
||||
}
|
||||
|
||||
Payment paymentConfig = wxPayConfigService.getPaymentConfigForStrategy(tenantId);
|
||||
Config wxPayConfig = wxPayConfigService.getWxPayConfig(tenantId);
|
||||
|
||||
InitiateBatchTransferRequest request = new InitiateBatchTransferRequest();
|
||||
request.setAppid(paymentConfig.getAppId());
|
||||
request.setOutBatchNo(outBatchNo);
|
||||
request.setBatchName(limitLen(batchName, 32));
|
||||
request.setBatchRemark(limitLen(remark, 32));
|
||||
request.setTotalAmount(amountFen);
|
||||
request.setTotalNum(1);
|
||||
|
||||
// 可选:复用支付通知地址(如配置为 https),用于接收转账结果通知
|
||||
if (StrUtil.isNotBlank(paymentConfig.getNotifyUrl()) && paymentConfig.getNotifyUrl().startsWith("https://")) {
|
||||
request.setNotifyUrl(paymentConfig.getNotifyUrl());
|
||||
}
|
||||
|
||||
TransferDetailInput detail = new TransferDetailInput();
|
||||
detail.setOutDetailNo(outDetailNo);
|
||||
detail.setTransferAmount(amountFen);
|
||||
detail.setTransferRemark(limitLen(remark, 32));
|
||||
detail.setOpenid(openid);
|
||||
if (StrUtil.isNotBlank(userName)) {
|
||||
detail.setUserName(userName);
|
||||
}
|
||||
request.setTransferDetailList(Collections.singletonList(detail));
|
||||
|
||||
try {
|
||||
TransferBatchService service = new TransferBatchService.Builder().config(wxPayConfig).build();
|
||||
InitiateBatchTransferResponse response = service.initiateBatchTransfer(request);
|
||||
log.info("微信商家转账已受理: tenantId={}, outBatchNo={}, batchId={}, batchStatus={}",
|
||||
tenantId, response.getOutBatchNo(), response.getBatchId(), response.getBatchStatus());
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("微信商家转账失败: tenantId={}, outBatchNo={}, openid={}, amountFen={}, err={}",
|
||||
tenantId, outBatchNo, openid, amountFen, e.getMessage(), e);
|
||||
throw PaymentException.systemError("微信商家转账失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String limitLen(String s, int maxLen) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
if (s.length() <= maxLen) {
|
||||
return s;
|
||||
}
|
||||
return s.substring(0, maxLen);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ import com.gxwebsoft.shop.entity.ShopDealerWithdraw;
|
||||
import com.gxwebsoft.shop.param.ShopDealerWithdrawParam;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.core.web.PageParam;
|
||||
import com.gxwebsoft.common.core.web.BatchParam;
|
||||
import com.gxwebsoft.common.core.annotation.OperationLog;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.payment.exception.PaymentException;
|
||||
import com.gxwebsoft.payment.service.WxTransferService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -20,7 +21,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@@ -38,6 +38,8 @@ public class ShopDealerWithdrawController extends BaseController {
|
||||
private ShopDealerWithdrawService shopDealerWithdrawService;
|
||||
@Resource
|
||||
private ShopDealerUserService shopDealerUserService;
|
||||
@Resource
|
||||
private WxTransferService wxTransferService;
|
||||
|
||||
@PreAuthorize("hasAuthority('shop:shopDealerWithdraw:list')")
|
||||
@Operation(summary = "分页查询分销商提现明细表")
|
||||
@@ -86,19 +88,84 @@ public class ShopDealerWithdrawController extends BaseController {
|
||||
|
||||
@PreAuthorize("hasAuthority('shop:shopDealerWithdraw:update')")
|
||||
@OperationLog
|
||||
@Transactional(rollbackFor = {Exception.class})
|
||||
@Operation(summary = "修改分销商提现明细表")
|
||||
@PutMapping()
|
||||
public ApiResult<?> update(@RequestBody ShopDealerWithdraw shopDealerWithdraw) {
|
||||
// 驳回操作,退回金额
|
||||
if(shopDealerWithdraw.getApplyStatus().equals(30)){
|
||||
final ShopDealerUser dealerUser = shopDealerUserService.getByUserIdRel(shopDealerWithdraw.getUserId());
|
||||
dealerUser.setMoney(dealerUser.getMoney().add(shopDealerWithdraw.getMoney()));
|
||||
if (shopDealerWithdraw.getId() == null) {
|
||||
return fail("id不能为空");
|
||||
}
|
||||
|
||||
// 以DB原数据为准,避免前端未传字段导致逻辑判断错误/金额被篡改等问题
|
||||
ShopDealerWithdraw db = shopDealerWithdrawService.getByIdRel(shopDealerWithdraw.getId());
|
||||
if (db == null) {
|
||||
return fail("提现记录不存在");
|
||||
}
|
||||
|
||||
Integer newStatus = shopDealerWithdraw.getApplyStatus() != null ? shopDealerWithdraw.getApplyStatus() : db.getApplyStatus();
|
||||
Integer oldStatus = db.getApplyStatus();
|
||||
Integer payType = shopDealerWithdraw.getPayType() != null ? shopDealerWithdraw.getPayType() : db.getPayType();
|
||||
|
||||
// 驳回操作(状态迁移到30时),退回金额,避免重复退回
|
||||
if (Integer.valueOf(30).equals(newStatus) && !Integer.valueOf(30).equals(oldStatus)) {
|
||||
ShopDealerUser dealerUser = shopDealerUserService.getByUserIdRel(db.getUserId());
|
||||
if (dealerUser != null && dealerUser.getMoney() != null && db.getMoney() != null) {
|
||||
dealerUser.setMoney(dealerUser.getMoney().add(db.getMoney()));
|
||||
shopDealerUserService.updateById(dealerUser);
|
||||
}
|
||||
// 已打款,要求上传凭证
|
||||
if(shopDealerWithdraw.getApplyStatus().equals(40)){
|
||||
}
|
||||
|
||||
// 微信收款且审核通过(迁移到20时),自动发起商家转账到零钱,并将状态置为已打款(40)
|
||||
if (Integer.valueOf(10).equals(payType)
|
||||
&& Integer.valueOf(20).equals(newStatus)
|
||||
&& !Integer.valueOf(20).equals(oldStatus)
|
||||
&& !Integer.valueOf(40).equals(oldStatus)) {
|
||||
|
||||
Integer tenantId = db.getTenantId() != null ? db.getTenantId() : getTenantId();
|
||||
if (tenantId == null) {
|
||||
return fail("tenantId为空,无法发起微信转账");
|
||||
}
|
||||
|
||||
String openid = StrUtil.isNotBlank(db.getOpenId()) ? db.getOpenId() : db.getOfficeOpenid();
|
||||
if (StrUtil.isBlank(openid)) {
|
||||
// 兜底:从分销商信息关联获取openid
|
||||
ShopDealerUser dealerUser = shopDealerUserService.getByUserIdRel(db.getUserId());
|
||||
if (dealerUser != null && StrUtil.isNotBlank(dealerUser.getOpenid())) {
|
||||
openid = dealerUser.getOpenid();
|
||||
}
|
||||
}
|
||||
if (StrUtil.isBlank(openid)) {
|
||||
return fail("用户openid为空,无法发起微信转账");
|
||||
}
|
||||
|
||||
String outBatchNo = "WD" + db.getId();
|
||||
String outDetailNo = "WD" + db.getId() + "D1";
|
||||
String remark = "分销商提现";
|
||||
String userName = db.getRealName();
|
||||
|
||||
try {
|
||||
wxTransferService.initiateSingleTransfer(
|
||||
tenantId,
|
||||
openid,
|
||||
db.getMoney(),
|
||||
outBatchNo,
|
||||
outDetailNo,
|
||||
"分销商提现",
|
||||
remark,
|
||||
userName
|
||||
);
|
||||
} catch (PaymentException e) {
|
||||
return fail(e.getMessage());
|
||||
}
|
||||
|
||||
shopDealerWithdraw.setApplyStatus(40);
|
||||
shopDealerWithdraw.setAuditTime(LocalDateTime.now());
|
||||
if(StrUtil.isBlankIfStr(shopDealerWithdraw.getImage())){
|
||||
}
|
||||
|
||||
// 已打款:非微信自动转账的场景,要求上传凭证
|
||||
if (Integer.valueOf(40).equals(newStatus)) {
|
||||
shopDealerWithdraw.setAuditTime(LocalDateTime.now());
|
||||
if (!Integer.valueOf(10).equals(payType) && StrUtil.isBlankIfStr(shopDealerWithdraw.getImage())) {
|
||||
return fail("请上传打款凭证");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user