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 * 发起单笔“商家转账到零钱”(升级版接口 /v3/fund-app/mch-transfer/transfer-bills
* *
* 默认:不需要用户在小程序确认页二次确认(直接受理转账)。
*
* @param tenantId 租户ID用于获取微信支付配置 * @param tenantId 租户ID用于获取微信支付配置
* @param openid 收款用户openid必须是该appid下的openid * @param openid 收款用户openid必须是该appid下的openid
* @param amountYuan 转账金额(单位:元) * @param amountYuan 转账金额(单位:元)
@@ -81,6 +83,30 @@ public class WxTransferService {
String outBillNo, String outBillNo,
String remark, String remark,
String userName) throws PaymentException { 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) { if (tenantId == null) {
throw PaymentException.paramError("租户ID不能为空"); throw PaymentException.paramError("租户ID不能为空");
@@ -149,6 +175,8 @@ public class WxTransferService {
request.setTransferAmount(amountFen); request.setTransferAmount(amountFen);
request.setTransferRemark(limitLen(remark, 32)); request.setTransferRemark(limitLen(remark, 32));
request.setTransferSceneReportInfos(sceneReportInfos); request.setTransferSceneReportInfos(sceneReportInfos);
// 小程序拉起收款确认页:需要 user_confirm=true微信会在响应中返回 package_info
request.setUserConfirm(userConfirm ? Boolean.TRUE : null);
log.debug("微信商家转账(升级版)请求参数: tenantId={}, outBillNo={}, transferSceneId={}, sceneReportInfosCount={}", log.debug("微信商家转账(升级版)请求参数: tenantId={}, outBillNo={}, transferSceneId={}, sceneReportInfosCount={}",
tenantId, outBillNo, transferSceneId, sceneReportInfos.size()); tenantId, outBillNo, transferSceneId, sceneReportInfos.size());
@@ -191,8 +219,9 @@ public class WxTransferService {
} }
} }
TransferBillsResponse response = httpResponse.getServiceResponse(); TransferBillsResponse response = httpResponse.getServiceResponse();
log.info("微信商家转账已受理(升级版): tenantId={}, outBillNo={}, transferBillNo={}, state={}", log.info("微信商家转账已受理(升级版): tenantId={}, outBillNo={}, userConfirm={}, transferBillNo={}, state={}",
tenantId, outBillNo, tenantId, outBillNo,
userConfirm,
response != null ? response.getTransferBillNo() : null, response != null ? response.getTransferBillNo() : null,
response != null ? response.getState() : null); response != null ? response.getState() : null);
return response; return response;
@@ -252,6 +281,10 @@ public class WxTransferService {
private String userName; private String userName;
private String notifyUrl; private String notifyUrl;
private List<TransferSceneReportInfo> transferSceneReportInfos; private List<TransferSceneReportInfo> transferSceneReportInfos;
/**
* 是否需要用户在小程序确认页确认收款;为 true 时,微信会返回 package_info用于 wx.requestMerchantTransfer。
*/
private Boolean userConfirm;
} }
@lombok.Data @lombok.Data
@@ -271,5 +304,7 @@ public class WxTransferService {
private String transferBillNo; private String transferBillNo;
private String createTime; private String createTime;
private String state; 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 io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 分销商提现明细表控制器 * 分销商提现明细表控制器
@@ -70,20 +73,100 @@ public class ShopDealerWithdrawController extends BaseController {
@Operation(summary = "添加分销商提现明细表") @Operation(summary = "添加分销商提现明细表")
@PostMapping() @PostMapping()
public ApiResult<?> save(@RequestBody ShopDealerWithdraw shopDealerWithdraw) { public ApiResult<?> save(@RequestBody ShopDealerWithdraw shopDealerWithdraw) {
try {
// 记录当前登录用户id // 记录当前登录用户id
User loginUser = getLoginUser(); User loginUser = getLoginUser();
if (loginUser != null) { 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()); 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()); 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())); dealerUser.setMoney(dealerUser.getMoney().subtract(shopDealerWithdraw.getMoney()));
shopDealerUserService.updateById(dealerUser); if (!shopDealerUserService.updateById(dealerUser)) {
if (shopDealerWithdrawService.save(shopDealerWithdraw)) { 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("添加成功"); 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')") @PreAuthorize("hasAuthority('shop:shopDealerWithdraw:update')")
@Transactional(rollbackFor = {Exception.class}) @Transactional(rollbackFor = {Exception.class})