feat(withdraw): 优化分销商提现流程并支持微信收款确认
- 添加提现方式必填校验,确保 payType 不为空 - 调整资金安全机制,申请后统一进入待审核状态(10),审核通过后用户主动领取 - 移除旧的微信提现逻辑,简化基础提现功能 - 增加防御性代码,防止前端未传字段时被更新为 NULL - 修改审核通过逻辑,仅标记为 20 状态,等待用户主动领取 - 阻止后台直接设置微信提现已打款状态(40),需用户领取后自动完成 - 添加非微信转账场景的打款凭证上传要求 - 新增 receive 接口供用户领取提现,返回微信收款确认页 package_info - 新增 receive-success 回调接口将状态置为已打款(40)
This commit is contained in:
@@ -93,6 +93,13 @@ public class ShopDealerWithdrawController extends BaseController {
|
|||||||
shopDealerWithdraw.setUserId(loginUser.getUserId());
|
shopDealerWithdraw.setUserId(loginUser.getUserId());
|
||||||
|
|
||||||
Integer payType = shopDealerWithdraw.getPayType();
|
Integer payType = shopDealerWithdraw.getPayType();
|
||||||
|
if (payType == null) {
|
||||||
|
return fail("提现方式不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 资金安全:用户申请后统一进入“待审核(10)”,审核通过后再由用户主动领取
|
||||||
|
shopDealerWithdraw.setApplyStatus(10);
|
||||||
|
shopDealerWithdraw.setAuditTime(null);
|
||||||
|
|
||||||
final ShopDealerUser dealerUser = shopDealerUserService.getByUserIdRel(loginUser.getUserId());
|
final ShopDealerUser dealerUser = shopDealerUserService.getByUserIdRel(loginUser.getUserId());
|
||||||
if (dealerUser == null) {
|
if (dealerUser == null) {
|
||||||
@@ -102,20 +109,6 @@ public class ShopDealerWithdrawController extends BaseController {
|
|||||||
return fail("可提现佣金不足");
|
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)) {
|
if (!shopDealerWithdrawService.save(shopDealerWithdraw)) {
|
||||||
return fail("添加失败");
|
return fail("添加失败");
|
||||||
}
|
}
|
||||||
@@ -126,31 +119,6 @@ public class ShopDealerWithdrawController extends BaseController {
|
|||||||
throw PaymentException.systemError("扣减可提现佣金失败", null);
|
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) {
|
} catch (PaymentException e) {
|
||||||
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||||
@@ -176,9 +144,18 @@ public class ShopDealerWithdrawController extends BaseController {
|
|||||||
return fail("提现记录不存在");
|
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 newStatus = shopDealerWithdraw.getApplyStatus() != null ? shopDealerWithdraw.getApplyStatus() : db.getApplyStatus();
|
||||||
Integer oldStatus = db.getApplyStatus();
|
Integer oldStatus = db.getApplyStatus();
|
||||||
Integer payType = shopDealerWithdraw.getPayType() != null ? shopDealerWithdraw.getPayType() : db.getPayType();
|
Integer payType = shopDealerWithdraw.getPayType() != null ? shopDealerWithdraw.getPayType() : db.getPayType();
|
||||||
|
Integer incomingStatus = shopDealerWithdraw.getApplyStatus();
|
||||||
|
|
||||||
// 驳回操作(状态迁移到30时),退回金额,避免重复退回
|
// 驳回操作(状态迁移到30时),退回金额,避免重复退回
|
||||||
if (Integer.valueOf(30).equals(newStatus) && !Integer.valueOf(30).equals(oldStatus)) {
|
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)
|
if (Integer.valueOf(10).equals(payType)
|
||||||
&& Integer.valueOf(20).equals(newStatus)
|
&& Integer.valueOf(40).equals(incomingStatus)
|
||||||
&& !Integer.valueOf(20).equals(oldStatus)
|
|
||||||
&& !Integer.valueOf(40).equals(oldStatus)) {
|
&& !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();
|
Integer tenantId = db.getTenantId() != null ? db.getTenantId() : getTenantId();
|
||||||
if (tenantId == null) {
|
if (tenantId == null) {
|
||||||
@@ -209,43 +232,72 @@ public class ShopDealerWithdrawController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (StrUtil.isBlank(openid)) {
|
if (StrUtil.isBlank(openid)) {
|
||||||
return fail("用户openid为空,无法发起微信转账");
|
return fail("用户openid为空,无法拉起微信收款确认页");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 微信支付商家转账接口对商户单号通常有最小长度限制(当前实测为 >= 5)。
|
// 使用提现记录ID构造单号,保持幂等;微信要求 5-32 且仅字母/数字
|
||||||
// 使用 0 填充,避免如 "WD59" 这种过短导致 PARAM_ERROR。
|
|
||||||
String outBillNo = String.format("WD%03d", db.getId());
|
String outBillNo = String.format("WD%03d", db.getId());
|
||||||
String remark = "分销商提现";
|
String remark = "分销商提现";
|
||||||
String userName = db.getRealName();
|
String userName = db.getRealName();
|
||||||
|
|
||||||
try {
|
WxTransferService.TransferBillsResponse resp = wxTransferService.initiateSingleTransferWithUserConfirm(
|
||||||
wxTransferService.initiateSingleTransfer(
|
tenantId,
|
||||||
tenantId,
|
openid,
|
||||||
openid,
|
db.getMoney(),
|
||||||
db.getMoney(),
|
outBillNo,
|
||||||
outBillNo,
|
remark,
|
||||||
remark,
|
userName
|
||||||
userName
|
);
|
||||||
);
|
if (resp == null || StrUtil.isBlank(resp.getPackageInfo())) {
|
||||||
} catch (PaymentException e) {
|
return fail("后台未返回 package_info,无法调起微信收款确认页");
|
||||||
return fail(e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shopDealerWithdraw.setApplyStatus(40);
|
Map<String, Object> data = new HashMap<>();
|
||||||
shopDealerWithdraw.setAuditTime(LocalDateTime.now());
|
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不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已打款:非微信自动转账的场景,要求上传凭证
|
ShopDealerWithdraw db = shopDealerWithdrawService.getByIdRel(id);
|
||||||
if (Integer.valueOf(40).equals(newStatus)) {
|
if (db == null) {
|
||||||
shopDealerWithdraw.setAuditTime(LocalDateTime.now());
|
return fail("提现记录不存在");
|
||||||
if (!Integer.valueOf(10).equals(payType) && StrUtil.isBlankIfStr(shopDealerWithdraw.getImage())) {
|
|
||||||
return fail("请上传打款凭证");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (shopDealerWithdrawService.updateById(shopDealerWithdraw)) {
|
if (!loginUser.getUserId().equals(db.getUserId())) {
|
||||||
return success("修改成功");
|
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')")
|
@PreAuthorize("hasAuthority('shop:shopDealerWithdraw:remove')")
|
||||||
|
|||||||
Reference in New Issue
Block a user