feat(withdraw): 实现微信小程序提现确认功能

- 在ShopDealerWithdrawController中添加微信提现流程的完整实现
- 新增initiateSingleTransferWithUserConfirm方法支持小程序拉起收款确认页
- 添加用户openid验证和package_info返回逻辑
- 实现事务回滚机制处理支付异常情况
- 增加提现金额验证和分销商信息校验
- 添加详细的错误处理和用户提示信息
- 更新WxTransferService支持用户确认模式的转账接口
This commit is contained in:
2026-01-31 16:16:35 +08:00
parent f9c693533c
commit 49998c71e4
2 changed files with 130 additions and 12 deletions

View File

@@ -68,6 +68,8 @@ public class WxTransferService {
/**
* 发起单笔“商家转账到零钱”(升级版接口 /v3/fund-app/mch-transfer/transfer-bills
*
* 默认:不需要用户在小程序确认页二次确认(直接受理转账)。
*
* @param tenantId 租户ID用于获取微信支付配置
* @param openid 收款用户openid必须是该appid下的openid
* @param amountYuan 转账金额(单位:元)
@@ -81,6 +83,30 @@ public class WxTransferService {
String outBillNo,
String remark,
String userName) throws PaymentException {
return initiateSingleTransferInternal(tenantId, openid, amountYuan, outBillNo, remark, userName, false);
}
/**
* 发起单笔“商家转账到零钱(小程序前端拉起收款确认页)”。
*
* 返回的 response.packageInfoJSON 字段 package_info用于小程序端调用 wx.requestMerchantTransfer。
*/
public TransferBillsResponse initiateSingleTransferWithUserConfirm(Integer tenantId,
String openid,
BigDecimal amountYuan,
String outBillNo,
String remark,
String userName) throws PaymentException {
return initiateSingleTransferInternal(tenantId, openid, amountYuan, outBillNo, remark, userName, true);
}
private TransferBillsResponse initiateSingleTransferInternal(Integer tenantId,
String openid,
BigDecimal amountYuan,
String outBillNo,
String remark,
String userName,
boolean userConfirm) throws PaymentException {
if (tenantId == null) {
throw PaymentException.paramError("租户ID不能为空");
@@ -149,6 +175,8 @@ public class WxTransferService {
request.setTransferAmount(amountFen);
request.setTransferRemark(limitLen(remark, 32));
request.setTransferSceneReportInfos(sceneReportInfos);
// 小程序拉起收款确认页:需要 user_confirm=true微信会在响应中返回 package_info
request.setUserConfirm(userConfirm ? Boolean.TRUE : null);
log.debug("微信商家转账(升级版)请求参数: tenantId={}, outBillNo={}, transferSceneId={}, sceneReportInfosCount={}",
tenantId, outBillNo, transferSceneId, sceneReportInfos.size());
@@ -191,8 +219,9 @@ public class WxTransferService {
}
}
TransferBillsResponse response = httpResponse.getServiceResponse();
log.info("微信商家转账已受理(升级版): tenantId={}, outBillNo={}, transferBillNo={}, state={}",
log.info("微信商家转账已受理(升级版): tenantId={}, outBillNo={}, userConfirm={}, transferBillNo={}, state={}",
tenantId, outBillNo,
userConfirm,
response != null ? response.getTransferBillNo() : null,
response != null ? response.getState() : null);
return response;
@@ -252,6 +281,10 @@ public class WxTransferService {
private String userName;
private String notifyUrl;
private List<TransferSceneReportInfo> transferSceneReportInfos;
/**
* 是否需要用户在小程序确认页确认收款;为 true 时,微信会返回 package_info用于 wx.requestMerchantTransfer。
*/
private Boolean userConfirm;
}
@lombok.Data
@@ -271,5 +304,7 @@ public class WxTransferService {
private String transferBillNo;
private String createTime;
private String state;
@SerializedName(value = "package_info", alternate = {"packageInfo"})
private String packageInfo;
}
}

View File

@@ -18,11 +18,14 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 分销商提现明细表控制器
@@ -70,19 +73,99 @@ public class ShopDealerWithdrawController extends BaseController {
@Operation(summary = "添加分销商提现明细表")
@PostMapping()
public ApiResult<?> save(@RequestBody ShopDealerWithdraw shopDealerWithdraw) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
shopDealerWithdraw.setUserId(loginUser.getUserId());
final ShopDealerUser dealerUser = shopDealerUserService.getByUserIdRel(loginUser.getUserId());
// 扣除提现金额
dealerUser.setMoney(dealerUser.getMoney().subtract(shopDealerWithdraw.getMoney()));
shopDealerUserService.updateById(dealerUser);
if (shopDealerWithdrawService.save(shopDealerWithdraw)) {
try {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("未登录或登录已失效");
}
if (shopDealerWithdraw.getMoney() == null || shopDealerWithdraw.getMoney().compareTo(java.math.BigDecimal.ZERO) <= 0) {
return fail("提现金额必须大于0");
}
Integer tenantId = shopDealerWithdraw.getTenantId() != null ? shopDealerWithdraw.getTenantId() : getTenantId();
if (tenantId == null) {
return fail("tenantId为空无法发起提现");
}
shopDealerWithdraw.setTenantId(tenantId);
shopDealerWithdraw.setUserId(loginUser.getUserId());
// 微信提现改为“小程序拉起收款确认页”,无需后台审核;其余方式仍按待审核处理
Integer payType = shopDealerWithdraw.getPayType();
if (Integer.valueOf(10).equals(payType)) {
shopDealerWithdraw.setApplyStatus(20);
shopDealerWithdraw.setAuditTime(LocalDateTime.now());
} else if (shopDealerWithdraw.getApplyStatus() == null) {
shopDealerWithdraw.setApplyStatus(10);
}
final ShopDealerUser dealerUser = shopDealerUserService.getByUserIdRel(loginUser.getUserId());
if (dealerUser == null) {
return fail("分销商信息不存在");
}
if (dealerUser.getMoney() == null || dealerUser.getMoney().compareTo(shopDealerWithdraw.getMoney()) < 0) {
return fail("可提现佣金不足");
}
// 微信提现:需要收款人 openid否则后续无法返回 package_info 拉起确认页
String wechatOpenid = null;
if (Integer.valueOf(10).equals(payType)) {
wechatOpenid = StrUtil.isNotBlank(shopDealerWithdraw.getOpenId())
? shopDealerWithdraw.getOpenId()
: shopDealerWithdraw.getOfficeOpenid();
if (StrUtil.isBlank(wechatOpenid) && StrUtil.isNotBlank(dealerUser.getOpenid())) {
wechatOpenid = dealerUser.getOpenid();
}
if (StrUtil.isBlank(wechatOpenid)) {
return fail("用户openid为空无法拉起微信收款确认页");
}
}
if (!shopDealerWithdrawService.save(shopDealerWithdraw)) {
return fail("添加失败");
}
// 扣除提现金额
dealerUser.setMoney(dealerUser.getMoney().subtract(shopDealerWithdraw.getMoney()));
if (!shopDealerUserService.updateById(dealerUser)) {
throw PaymentException.systemError("扣减可提现佣金失败", null);
}
// 微信提现:后端先创建“商家转账单(需用户确认)”,返回 package_info 给小程序调用 wx.requestMerchantTransfer
if (Integer.valueOf(10).equals(payType)) {
// 微信支付商家转账接口对商户单号通常有最小长度限制(当前实测为 >= 5
String outBillNo = String.format("WD%03d", shopDealerWithdraw.getId());
String remark = "分销商提现";
String userName = shopDealerWithdraw.getRealName();
WxTransferService.TransferBillsResponse resp = wxTransferService.initiateSingleTransferWithUserConfirm(
tenantId,
wechatOpenid,
shopDealerWithdraw.getMoney(),
outBillNo,
remark,
userName
);
if (resp == null || StrUtil.isBlank(resp.getPackageInfo())) {
throw PaymentException.systemError("后台未返回 package_info无法调起微信收款确认页", null);
}
Map<String, Object> data = new HashMap<>();
data.put("package_info", resp.getPackageInfo());
return success("添加成功", data);
}
return success("添加成功");
}
} catch (PaymentException e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return fail(e.getMessage());
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return fail("添加失败");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('shop:shopDealerWithdraw:update')")