feat(withdraw): 优化分销商提现流程并支持微信收款确认

- 添加提现方式必填校验,确保 payType 不为空
- 调整资金安全机制,申请后统一进入待审核状态(10),审核通过后用户主动领取
- 移除旧的微信提现逻辑,简化基础提现功能
- 增加防御性代码,防止前端未传字段时被更新为 NULL
- 修改审核通过逻辑,仅标记为 20 状态,等待用户主动领取
- 阻止后台直接设置微信提现已打款状态(40),需用户领取后自动完成
- 添加非微信转账场景的打款凭证上传要求
- 新增 receive 接口供用户领取提现,返回微信收款确认页 package_info
- 新增 receive-success 回调接口将状态置为已打款(40)
This commit is contained in:
2026-01-31 22:21:11 +08:00
parent 940e96f59d
commit 7f7b7527a0

View File

@@ -93,6 +93,13 @@ public class ShopDealerWithdrawController extends BaseController {
shopDealerWithdraw.setUserId(loginUser.getUserId());
Integer payType = shopDealerWithdraw.getPayType();
if (payType == null) {
return fail("提现方式不能为空");
}
// 资金安全:用户申请后统一进入“待审核(10)”,审核通过后再由用户主动领取
shopDealerWithdraw.setApplyStatus(10);
shopDealerWithdraw.setAuditTime(null);
final ShopDealerUser dealerUser = shopDealerUserService.getByUserIdRel(loginUser.getUserId());
if (dealerUser == null) {
@@ -102,20 +109,6 @@ public class ShopDealerWithdrawController extends BaseController {
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("添加失败");
}
@@ -126,31 +119,6 @@ public class ShopDealerWithdrawController extends BaseController {
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();
@@ -176,9 +144,18 @@ public class ShopDealerWithdrawController extends BaseController {
return fail("提现记录不存在");
}
// 防御:前端未传字段时,避免被 updateById 更新为 NULL
if (shopDealerWithdraw.getApplyStatus() == null) {
shopDealerWithdraw.setApplyStatus(db.getApplyStatus());
}
if (shopDealerWithdraw.getPayType() == null) {
shopDealerWithdraw.setPayType(db.getPayType());
}
Integer newStatus = shopDealerWithdraw.getApplyStatus() != null ? shopDealerWithdraw.getApplyStatus() : db.getApplyStatus();
Integer oldStatus = db.getApplyStatus();
Integer payType = shopDealerWithdraw.getPayType() != null ? shopDealerWithdraw.getPayType() : db.getPayType();
Integer incomingStatus = shopDealerWithdraw.getApplyStatus();
// 驳回操作状态迁移到30时退回金额避免重复退回
if (Integer.valueOf(30).equals(newStatus) && !Integer.valueOf(30).equals(oldStatus)) {
@@ -189,11 +166,57 @@ public class ShopDealerWithdrawController extends BaseController {
}
}
// 微信收款且审核通过迁移到20时自动发起商家转账到零钱并将状态置为已打款(40)
// 审核通过:仅标记为 20等待用户在提现记录中主动领取微信等在线转账在领取环节执行
if (Integer.valueOf(20).equals(incomingStatus) && !Integer.valueOf(20).equals(oldStatus)) {
shopDealerWithdraw.setAuditTime(LocalDateTime.now());
}
// 微信提现:已打款(40)由用户“领取成功”后置为 40后台不允许直接改为 40
if (Integer.valueOf(10).equals(payType)
&& Integer.valueOf(20).equals(newStatus)
&& !Integer.valueOf(20).equals(oldStatus)
&& Integer.valueOf(40).equals(incomingStatus)
&& !Integer.valueOf(40).equals(oldStatus)) {
return fail("微信提现请用户在提现记录中领取后自动完成,后台不可直接置为已打款");
}
// 已打款:非微信自动转账的场景,要求上传凭证
if (Integer.valueOf(40).equals(incomingStatus) && !Integer.valueOf(40).equals(oldStatus)) {
shopDealerWithdraw.setAuditTime(LocalDateTime.now());
if (!Integer.valueOf(10).equals(payType) && StrUtil.isBlankIfStr(shopDealerWithdraw.getImage())) {
return fail("请上传打款凭证");
}
}
if (shopDealerWithdrawService.updateById(shopDealerWithdraw)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('shop:shopDealerWithdraw:update')")
@Operation(summary = "用户领取提现(审核通过后,返回微信收款确认页 package_info")
@PostMapping("/receive/{id}")
public ApiResult<?> receive(@PathVariable("id") Integer id) {
try {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("未登录或登录已失效");
}
if (id == null) {
return fail("id不能为空");
}
ShopDealerWithdraw db = shopDealerWithdrawService.getByIdRel(id);
if (db == null) {
return fail("提现记录不存在");
}
if (!loginUser.getUserId().equals(db.getUserId())) {
return fail("无权领取该提现记录");
}
if (!Integer.valueOf(20).equals(db.getApplyStatus())) {
return fail("当前状态不可领取");
}
if (!Integer.valueOf(10).equals(db.getPayType())) {
return fail("仅微信提现支持在线领取");
}
Integer tenantId = db.getTenantId() != null ? db.getTenantId() : getTenantId();
if (tenantId == null) {
@@ -209,43 +232,72 @@ public class ShopDealerWithdrawController extends BaseController {
}
}
if (StrUtil.isBlank(openid)) {
return fail("用户openid为空无法起微信转账");
return fail("用户openid为空无法起微信收款确认页");
}
// 微信支付商家转账接口对商户单号通常有最小长度限制(当前实测为 >= 5
// 使用 0 填充,避免如 "WD59" 这种过短导致 PARAM_ERROR。
// 使用提现记录ID构造单号保持幂等微信要求 5-32 且仅字母/数字
String outBillNo = String.format("WD%03d", db.getId());
String remark = "分销商提现";
String userName = db.getRealName();
try {
wxTransferService.initiateSingleTransfer(
tenantId,
openid,
db.getMoney(),
outBillNo,
remark,
userName
);
} catch (PaymentException e) {
return fail(e.getMessage());
WxTransferService.TransferBillsResponse resp = wxTransferService.initiateSingleTransferWithUserConfirm(
tenantId,
openid,
db.getMoney(),
outBillNo,
remark,
userName
);
if (resp == null || StrUtil.isBlank(resp.getPackageInfo())) {
return fail("后台未返回 package_info无法调起微信收款确认页");
}
shopDealerWithdraw.setApplyStatus(40);
shopDealerWithdraw.setAuditTime(LocalDateTime.now());
Map<String, Object> data = new HashMap<>();
data.put("package_info", resp.getPackageInfo());
return success("领取发起成功", data);
} catch (PaymentException e) {
return fail(e.getMessage());
} catch (Exception e) {
return fail("领取失败");
}
}
@Operation(summary = "用户领取成功回调(将状态置为已打款/已领取40")
@PostMapping("/receive-success/{id}")
public ApiResult<?> receiveSuccess(@PathVariable("id") Integer id) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("未登录或登录已失效");
}
if (id == null) {
return fail("id不能为空");
}
// 已打款:非微信自动转账的场景,要求上传凭证
if (Integer.valueOf(40).equals(newStatus)) {
shopDealerWithdraw.setAuditTime(LocalDateTime.now());
if (!Integer.valueOf(10).equals(payType) && StrUtil.isBlankIfStr(shopDealerWithdraw.getImage())) {
return fail("请上传打款凭证");
}
ShopDealerWithdraw db = shopDealerWithdrawService.getByIdRel(id);
if (db == null) {
return fail("提现记录不存在");
}
if (shopDealerWithdrawService.updateById(shopDealerWithdraw)) {
return success("修改成功");
if (!loginUser.getUserId().equals(db.getUserId())) {
return fail("无权操作该提现记录");
}
return fail("修改失败");
if (!Integer.valueOf(10).equals(db.getPayType())) {
return fail("仅微信提现支持在线领取");
}
if (Integer.valueOf(40).equals(db.getApplyStatus())) {
return success("已领取");
}
if (!Integer.valueOf(20).equals(db.getApplyStatus())) {
return fail("当前状态不可确认领取");
}
ShopDealerWithdraw upd = new ShopDealerWithdraw();
upd.setId(db.getId());
upd.setApplyStatus(40);
upd.setAuditTime(LocalDateTime.now());
if (shopDealerWithdrawService.updateById(upd)) {
return success("领取成功");
}
return fail("领取失败");
}
@PreAuthorize("hasAuthority('shop:shopDealerWithdraw:remove')")