From 49998c71e46bb1060bcb766da61a110af231dfde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sat, 31 Jan 2026 16:16:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(withdraw):=20=E5=AE=9E=E7=8E=B0=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=B0=8F=E7=A8=8B=E5=BA=8F=E6=8F=90=E7=8E=B0=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ShopDealerWithdrawController中添加微信提现流程的完整实现 - 新增initiateSingleTransferWithUserConfirm方法支持小程序拉起收款确认页 - 添加用户openid验证和package_info返回逻辑 - 实现事务回滚机制处理支付异常情况 - 增加提现金额验证和分销商信息校验 - 添加详细的错误处理和用户提示信息 - 更新WxTransferService支持用户确认模式的转账接口 --- .../payment/service/WxTransferService.java | 37 +++++- .../ShopDealerWithdrawController.java | 105 ++++++++++++++++-- 2 files changed, 130 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/gxwebsoft/payment/service/WxTransferService.java b/src/main/java/com/gxwebsoft/payment/service/WxTransferService.java index a5ebfb9..7032a2b 100644 --- a/src/main/java/com/gxwebsoft/payment/service/WxTransferService.java +++ b/src/main/java/com/gxwebsoft/payment/service/WxTransferService.java @@ -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.packageInfo(JSON 字段 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 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; } } diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopDealerWithdrawController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopDealerWithdrawController.java index b9f1f06..c6bcc53 100644 --- a/src/main/java/com/gxwebsoft/shop/controller/ShopDealerWithdrawController.java +++ b/src/main/java/com/gxwebsoft/shop/controller/ShopDealerWithdrawController.java @@ -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 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')")