feat(withdraw): 实现微信小程序提现确认功能
- 在ShopDealerWithdrawController中添加微信提现流程的完整实现 - 新增initiateSingleTransferWithUserConfirm方法支持小程序拉起收款确认页 - 添加用户openid验证和package_info返回逻辑 - 实现事务回滚机制处理支付异常情况 - 增加提现金额验证和分销商信息校验 - 添加详细的错误处理和用户提示信息 - 更新WxTransferService支持用户确认模式的转账接口
This commit is contained in:
@@ -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.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) {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
Reference in New Issue
Block a user