feat(withdraw): 实现分销商提现微信自动转账功能

- 新增 WxTransferService 服务类实现微信商家转账到零钱功能
- 在 ShopDealerWithdrawController 中集成微信转账服务
- 修改 update 方法支持微信收款方式的自动转账处理
- 添加事务管理确保转账操作的数据一致性
- 实现转账参数验证和错误处理机制
- 支持通过 openid 自动获取和用户姓名验证
- 添加转账金额转换和批次号生成逻辑
This commit is contained in:
2026-01-29 00:28:47 +08:00
parent a3e812a9c4
commit d93dd04211
2 changed files with 216 additions and 13 deletions

View File

@@ -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);
}
}

View File

@@ -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("请上传打款凭证");
}
}