微信提现 - 企业付款到零钱

2023年8月份做的提现,记录一下吧,等后面有更好的方案再来优化

大致的提现流程:
1.用户在微信小程序里输入金额后点击申请提现,请求到后端
2.后端进行提现前的效验,组装数据,保存此次用户的提现记录(若提现失败,后续还能继续提现)
3.数据组装完成,改状态,扣除冻结余额,扣提现手续费,后请求微信企业付款接口(这一步该先请求提现,失败就不用去改状态,写记录那些)
4.无论提现成功与否,保存本次请求微信企业支付日志
5.提现成功,返回提现成功信息。
6.提现失败,根据微信返回的错误码,返回相应的错误信息,并后续在后台管理里面二次处理

【进入正题】

1.首先从用户操作小程序提现开始
用户在小程序上提现
提现前验证手机号短信

2.小程序请求后端提现,带入的参数:申请金额,手机号,验证码,微信code
请求提现接口

// 申请提现-小程序
@PostMapping("/applyCashOut")
public AjaxResult applyCashOut(@RequestBody UserCashOutApplyDTO applyDTO){
    applyDTO.setOperateType(CashOutOperateType.AUTO.name());
    applyDTO.setUserId(getUserInfo().getUserId());
    userCashoutApplyService.applyCashOut(applyDTO);
    return AjaxResult.success("申请提现成功");
}

3.实现层里通过OperateType来判断是小程序提现还是后台手动处理提现

// 申请提现-小程序
@Override
@Transactional(rollbackFor = Exception.class)
public void applyCashOut(UserCashOutApplyDTO applyDTO) {
    //小程序自动提现
    if(CashOutOperateType.AUTO.name().equals(applyDTO.getOperateType())){
        //小程序提现效验
        verifyAutoCashOut(applyDTO);
        //保存提现申请
        UserCashoutApply cashOutApply = new UserCashoutApply(applyDTO);
        userCashoutApplyMapper.insertUserCashoutApply(cashOutApply);
        //保存存入时的自增ID
        applyDTO.setId(cashOutApply.getId());
        //调用异步提现
        asyncUserCashOut.cashOutAudit(applyDTO,this);
    }
    //后台手动提现
    if(CashOutOperateType.MANUAL.name().equals(applyDTO.getOperateType())){
        //后台提现效验
        verifyManualCashOut(applyDTO);
        //调用提现
        handleCashOutCashResult(applyDTO);
    }
}

4.【小程序提现效验】没啥说的,重点是用微信code获取openId,生成提现单号等,根据业务需要,全部代码如下:

//小程序提现效验
verifyAutoCashOut(applyDTO);

----------------------------------------------------------------------------
/**
 * 小程序自动提现效验
 * @param applyDTO
 */
public void verifyAutoCashOut(UserCashOutApplyDTO applyDTO){
    if(applyDTO.getUserId() == null){
        throw new ServiceException("用户ID为空");
    }
    if (StringUtils.isBlank(applyDTO.getPhone())) {
        throw new ServiceException("请输入手机号");
    }
    if(StringUtils.isEmpty(applyDTO.getOperateType())){
        throw new ServiceException("操作类型为空");
    }
    if (StringUtils.isEmpty(applyDTO.getWxCode())) {
        throw new ServiceException("请微信登录后再提现,微信code为空");
    }
    if(StringUtils.isEmpty(applyDTO.getCode())){
        throw new ServiceException("验证码为空");
    }

    //  手机短信验证
    String redisCodeKey = "send_" + SmsConstants.SMS_TYPE_CASH + SmsConstants.SMS_CACHE_CASH + applyDTO.getPhone();
    String redisCode = redisCache.getCacheObject(redisCodeKey);
    logger.info("手机号:'{}',获取缓存验证码:{}",applyDTO.getPhone(),redisCode);
    if (StringUtils.isBlank(redisCode)) {
        throw new ServiceException("验证码已过期,请重新发送");
    }
    if (!Objects.equals(applyDTO.getCode(), redisCode)) {
        throw new ServiceException("验证码错误");
    }

    Long userId = applyDTO.getUserId();
    UserInfo userInfo = userInfoService.selectUserInfoByUserId(userId);
    if(userInfo == null){
        throw new ServiceException("未查询到用户信息");
    }
    if (!Objects.equals(applyDTO.getPhone(), userInfo.getPhone())) {
        throw new ServiceException("请使用注册手机号验证");
    }

    //  判断是否开启提现
    String withdrawSwitch = businessConfigService.selectBusinessConfigByConfigType("WITHDRAW_SWITCH");
    if(StringUtils.isEmpty(withdrawSwitch) || UserConstants.NO.equals(withdrawSwitch)){
        throw new ServiceException("提现功能暂未开启");
    }
    //  判断是否提现中
    UserCashoutApply cashOutApply = new UserCashoutApply(userId,BalanceConstants.WITHDRAWAL_NO);
    List<UserCashoutApply> userCashOutApplies = userCashoutApplyMapper.selectUserCashoutApplyList(cashOutApply);
    if(!userCashOutApplies.isEmpty()){
        throw new ServiceException("已存在提现订单,请勿重复申请提现");
    }

    //  判断提现次数
    String withdrawNumber = businessConfigService.selectBusinessConfigByConfigType("WITHDRAW_NUMBER");
    Long maxNumber = StringUtils.isEmpty(withdrawNumber) ? 1 : Long.parseLong(withdrawNumber);
    List<UserCashoutApply> userCashoutApplies = userCashoutApplyService.selectUserCashoutApplyList(
            new UserCashoutApply(userId, BalanceConstants.WITHDRAWAL_YES, DateUtils.getDate()));
    if(userCashoutApplies.size() >= maxNumber){
        throw new ServiceException("今日提现次数已用完,请明天再来吧");
    }


    //  根据微信Code获取OpenID
    String openId = "";
    Map<String, String> webChatAccessToken = getWebChatAccessToken(applyDTO.getWxCode());
    if(webChatAccessToken == null){
        throw new ServiceException("无法解析到当前微信账号信息,请联系管理员");
    }
    openId = webChatAccessToken.get("openid");
    if(StringUtils.isBlank(openId)){
        logger.error("未获取到微信OpenId--->" + JSONObject.fromObject(webChatAccessToken));
        String errCode = webChatAccessToken.get("errcode");
        throw new ServiceException("检测到不合法的微信账号,请联系管理员;" + (errCode == null ? "" : errCode));
    }

    //  获取提现手续费
    String cashOutExpense = configService.selectBusinessConfigByConfigType(BusConf.CASH_OUT_EXPENSE.name());
    if(StringUtils.isEmpty(cashOutExpense)){
        cashOutExpense = "0";
    }
    BigDecimal expenseAmount = new BigDecimal(cashOutExpense);

    //  获取提现单号
    String withdrawSn = getCashOutNo();

    //  判断余额并冻结
    BigDecimal userBalance = this.accordAmount(userId, applyDTO.getApplyAmount(), expenseAmount, withdrawSn);

    //  判断是否公司员工提现
    UserSysuser userSysuser = userSysuserService.selectUserSysuserByUserId(userId);
    if(userSysuser != null){
        applyDTO.setUserType("EMPLOYEE");
    }
    //OpenId
    applyDTO.setOpenId(openId);
    //审核人ID
    applyDTO.setAuditUserId(-1L);
    //用户信息
    applyDTO.setUserInfo(userInfo);
    //到账备注
    applyDTO.setDesc("XXX小程序,余额提现自动到账,更多奇观集结请关注XXX小程序");
    //提现单号(生成)
    applyDTO.setCashOutNo(withdrawSn);
    //审核人
    applyDTO.setAuditUserName("XXX");
    //原始余额
    applyDTO.setOldAmount(userBalance);
    //提现手续费
    applyDTO.setCashOutExpense(expenseAmount);
}

5.【保存提现申请】就是将本次提现记录,保存到数据库。如提现失败,可以在后台进行二次操作
构造方法如下,将数据保存到数据库中

//保存提现申请
 UserCashoutApply cashOutApply = new UserCashoutApply(applyDTO);
 userCashoutApplyMapper.insertUserCashoutApply(cashOutApply);
 ------------------------------------------------------------------------------------
public UserCashoutApply(UserCashOutApplyDTO applyDTO) {
    this.applyTime = new Date();
    this.userId = applyDTO.getUserId();
    this.openId = applyDTO.getOpenId();
    this.cashoutNo = applyDTO.getCashOutNo();
    this.oldAmount = applyDTO.getOldAmount();
    this.applyAmount = applyDTO.getApplyAmount();
    this.status = BalanceConstants.WITHDRAWAL_NO;
    this.cashOutExpense = applyDTO.getCashOutExpense();
    this.userName = applyDTO.getUserInfo().getNickName();
}

6.【保存存入时的自增ID】这是为了在下一步提现操作中,修改提现记录里面的申请状态,提现成功信息或失败原因

  //保存存入时的自增ID
  applyDTO.setId(cashOutApply.getId());

7.【调用异步提现(也可以同步)】我这里是先做了提现成功后的处理,这里感觉没对,应该是提现成功后再做处理(比如:修改提现状态,扣除提现冻结金额,扣除提现手续费等),我这里是依赖异常回滚,如果提现状态不为成功,就回滚。如图:
调用提现前处理
7.1 调用提现前的处理,如果提现失败则会回滚

 //提现处理--改状态,扣除冻结余额,扣提现费用,记录总提现金额,写记录
 handleCashOut(applyDTO);
 -----------------------------------------------------------
 /**
 * 提现前----处理
 */
@Transactional(rollbackFor = Exception.class)
public void handleCashOut(UserCashOutApplyDTO applyDTO){
    UserInfo userInfo = userInfoService.selectUserInfoByUserId(applyDTO.getUserId());
    UserBalance balance = balanceService.selectUserBalanceByUserId(userInfo.getUserId());
    UserCashoutApply cashOutApply = userCashoutApplyMapper.selectUserCashoutApplyById(applyDTO.getId());
    if(balance == null){
        logger.error("未查询到用户余额;UserId ----> " + userInfo.getUserId());
        throw new ServiceException("未查询到用户余额");
    }
    //判断余额是否满足
    BigDecimal expenseAmount = applyDTO.getApplyAmount().add(applyDTO.getCashOutExpense());
    if(expenseAmount.compareTo(balance.getFrozenBalance()) > 0){
        logger.error("用户冻结提现余额不足;UserId:'{}',冻结余额:'{}',提现金额:'{}',提现手续费:'{}'",
                userInfo.getUserId(),balance.getFrozenBalance(),applyDTO.getApplyAmount(),applyDTO.getCashOutExpense());
        throw new ServiceException("冻结余额不足");
    }
    //修改提现申请状态
    cashOutApply.setCashoutTime(new Date());
    cashOutApply.setRemark(applyDTO.getDesc());
    cashOutApply.setErrorMsg("提现成功");
    cashOutApply.setAuditUserId(applyDTO.getAuditUserId());
    cashOutApply.setStatus(BalanceConstants.WITHDRAWAL_YES);
    cashOutApply.setAuditUserName(applyDTO.getAuditUserName());
    int i = userCashoutApplyMapper.updateUserCashoutApply(cashOutApply);
    if(i == 0){
        logger.error("版本锁不一致;提现ID ---> " + applyDTO.getId());
        throw new ServiceException("系统繁忙,请稍后再试");
    }
    //新增用户余额变动日志并扣减用户冻结余额
    saveCashOutBalanceLog(applyDTO);
}

7.2 提现接口参数组装

 //提现接口参数
 WeChatCashOut weChatCashOut = new WeChatCashOut(applyDTO);
 ------------------------------------------------------------
 public WeChatCashOut(UserCashOutApplyDTO cashOutApplyDTO) {
    //提现备注
    this.desc = cashOutApplyDTO.getDesc();
    //提现申请表ID
    this.cashOutId = cashOutApplyDTO.getId();
    //openID
    this.openId = cashOutApplyDTO.getOpenId();
    //提现单号
    this.cashOutNo = cashOutApplyDTO.getCashOutNo();
    //提现金额
    this.amount = String.valueOf(cashOutApplyDTO.getApplyAmount());
}

7.2 【调用提现接口】

 //调用提现接口
 Map<String, String> resultMap = weChatCashOutCash.wxCashOut(weChatCashOut);
 ----------------------------------------------------------------------------------------------------------------------
    /**
 * 企业付款到零钱接口
 */
public static final String WECHAT_WITHDRAW_CASH_API = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers";

/**
 * 企业付款到零钱-查询接口
 */
public static final String WECHAT_WITHDRAW_CASH_QUERY = "https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo";

 /**
 * 请求微信配置类(appId,商户ID,key,证书路径)
 */
private final WeChatCashOutConfig wxConfig;

public WeChatCashOutCash(WeChatCashOutConfig wxConfig) {
    this.wxConfig = wxConfig;
}

 /**
 * 调用微信提现-企业付款到零钱
 * WXPay,WXPayUtil 都是微信支付的SDK
 * @return
 */
public Map<String, String> wxCashOut(WeChatCashOut weChatCashOut) {
    log.info("提现订单:'{}',用户提现请求参数:'{}'",weChatCashOut.getCashOutNo(),weChatCashOut);
    Map<String, String> resultMap;
    Map<String, String> requestData = new LinkedHashMap<>();
    //微信支付的SDK,wxConfig 是关于微信的配置类(appId,商户ID,key,证书路径)
    WXPay wxPay = new WXPay(wxConfig);
    log.info("企业支付配置---> " + wxConfig);
    //appid
    requestData.put("mch_appid", wxConfig.getAppID());
    //商户号
    requestData.put("mchid", wxConfig.getMchID());
    //随机字符串
    requestData.put("nonce_str", WXPayUtil.generateNonceStr());
    //备注
    requestData.put("desc", weChatCashOut.getDesc());
    //(商户订单号-提现单号)唯一字符串
    requestData.put("partner_trade_no", weChatCashOut.getCashOutNo());
    //用户openid
    requestData.put("openid", weChatCashOut.getOpenId());
    //用户校验-不校验真实姓名
    requestData.put("check_name", "NO_CHECK");
    try {
        //金额
        requestData.put("amount", String.valueOf(DecimalUtils.yzf(weChatCashOut.getAmount())));
        //签名
        requestData.put("sign", WXPayUtil.generateSignature(requestData, wxConfig.getKey()));
        //请求微信
        String wxPayStr = wxPay.requestWithCert(
                WECHAT_WITHDRAW_CASH_API,
                requestData,
                wxConfig.getHttpConnectTimeoutMs(),
                wxConfig.getHttpReadTimeoutMs()
        );
        log.info("提现请求微信,返回结果:{}", wxPayStr);
        resultMap = WXPayUtil.xmlToMap(wxPayStr);
    } catch (Exception e) {
        log.error("提现错误:{}", e.getMessage());
        e.printStackTrace();
        if (e.getMessage() != null && e.getMessage().contains(WxPayResultStatus.ERR_CODE_DES.getVal())) {
            String requestMsg = "微信企业付款零钱失败:" + e.getMessage().substring(
                            e.getMessage().indexOf("err_code_des><![CDATA[") + 22,
                            e.getMessage().indexOf("]]></err_code_des>"));
            log.error(requestMsg);
            throw new ServiceException(requestMsg);
        }
        throw new ServiceException("微信提现失败,请稍后重试");
    }
    if (!WxPayStatus.SUCCESS.name().equalsIgnoreCase(resultMap.get(WxPayResultStatus.RESULT_CODE.getVal()))) {
        WeChatResultError resultError = new WeChatResultError(
                resultMap.get(WxPayResultStatus.ERR_CODE.getVal()),
                resultMap.get(WxPayResultStatus.ERR_CODE_DES.getVal())
        );
        //微信付款失败提示,按微信给的错误码提示
        this.wxThrowMessage(resultError,weChatCashOut.getCashOutId());
    }
    return resultMap;
}

微信支付SDK
请添加图片描述

WeChatCashOutConfig 配置类,详情见下面代码:

//请求微信配置类(appId,商户ID,key,证书路径)
private final WeChatCashOutConfig wxConfig;

public WeChatCashOutCash(WeChatCashOutConfig wxConfig) {
    this.wxConfig = wxConfig;
}
这里面需要
 -------------------------------------------------------------------------
@Component
public class WeChatCashOutConfig implements WXPayConfig {

private final Logger log = LoggerFactory.getLogger(WeChatCashOutConfig.class);

/**
 *appid
 */
@Value("${wx.applets.appId}")
public String appId;

/**
 *商户id
 */
@Value("${wx.mch.mchId}")
public String mchId;

/**
 *key
 */
@Value("${wx.mch.mchApiKey}")
public String key;

/**
 *证书路径
 */
@Value("${wx.mch.mchCretP12}")
public String certPath;

private byte[] certData;


@Override
public String getAppID() {
    return appId;
}

@Override
public String getMchID() {
    return mchId;
}

@Override
public String getKey() {
    return key;
}

@Override
public InputStream getCertStream() {
    try {
        System.out.println("证书目录-----> " + certPath);
        File file = new File(certPath);
        certData = new byte[(int) file.length()];
        FileInputStream certStream = new FileInputStream(file);
        int read = certStream.read(certData);
        certStream.close();
    } catch (Exception e) {
        log.error("微信支付证书加载错误 -文件:" + certPath + "没找到");
        e.printStackTrace();
    }
    return new ByteArrayInputStream(certData);
}

@Override
public int getHttpConnectTimeoutMs() {
    return 0;
}

@Override
public int getHttpReadTimeoutMs() {
    return 0;
}


@Override
public String toString() {
    return "WeChatCashOutConfig{" +
            "appId='" + appId + '\'' +
            ", mchId='" + mchId + '\'' +
            ", key='" + key + '\'' +
            ", certPath='" + certPath + '\'' +
            ", certData=" + Arrays.toString(certData) +
            '}';
	}
}

WeChatCashOutConfig配置类里的数据,需要在yml里面配置你自己的,如图:

请添加图片描述

微信付款失败提示(wxThrowMessage):

//微信付款失败提示
this.wxThrowMessage(resultError,weChatCashOut.getCashOutId());
 -------------------------------------------------------------------------
/**
 * 微信付款失败提示
 */
private void wxThrowMessage(WeChatResultError resultError,Long cashOutId) {
    String errorMsg;
    String errorCode = resultError.getErrCode();
    if (WxPayError.FREQ_LIMIT.name().equals(errorCode)) {
        log.error("微信提现零钱接口请求频率超限制");
        errorMsg = "微信提现零钱接口请求频率超限制";
    } else if (WxPayError.SYSTEMERROR.name().equals(errorCode)) {
        log.error("微信提现零钱接口返回系统繁忙");
        errorMsg = "微信提现零钱接口请求频率超限制";
    } else if (WxPayError.SENDNUM_LIMIT.name().equals(errorCode)) {
        log.error("今日付款次数超过限制,如有需要请进入【微信支付商户平台-产品中心-企业付款到零钱-产品设置】进行修改");
        errorMsg = "今日付款次数超过限制,如有需要请进入【微信支付商户平台-产品中心-企业付款到零钱-产品设置】进行修改";
    } else if (WxPayError.NOTENOUGH.name().equals(errorCode)) {
        log.error("商户余额不足,原因:您的付款帐号余额不足或资金未到账");
        errorMsg = "商户余额不足,原因:您的付款帐号余额不足或资金未到账";
    } else if (WxPayError.V2_ACCOUNT_SIMPLE_BAN.name().equals(errorCode)) {
        log.error("用户微信支付账户未实名,无法付款");
        errorMsg = "用户微信支付账户未实名,无法付款";
    } else if (WxPayError.MONEY_LIMIT.name().equals(errorCode)) {
        log.error("商户号或个人账户限额!");
        errorMsg = "商户号或个人账户限额";
    } else {
        log.error("code:"+ errorCode +",message:" + resultError.getErrCodeDes());
        errorMsg = "未知错误:code:"+ errorCode +",message:" + resultError.getErrCodeDes()
        + "付款到零钱错误码查询:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2";
    }
    //调用异步保存错误信息
    SpringUtils.getBean(AsyncUserCashOut.class).saveErrorMsgCashOutApply(cashOutId,errorMsg);
    if(errorMsg.contains("未知错误")){
        errorMsg = "今日暂时无法提现,请明日再试";
    }
    throw new ServiceException(errorMsg);
}

7.3 保存微信企业付款请求日志

  //微信企业付款请求日志
  wxRequestLog(weChatCashOut,resultMap);
  ---------------------------------------------------------------------------------
 /**
 * 微信企业付款请求日志
 * @param weChatCashOut
 * @param resultMap
 */
private void wxRequestLog(WeChatCashOut weChatCashOut,Map<String, String> resultMap){
    WechatCashoutLog wechatCashoutLog = new WechatCashoutLog();
    UserCashoutApply cashOutApply = userCashoutApplyService.selectUserCashoutApplyByOrderNo(weChatCashOut.getCashOutNo());
    wechatCashoutLog.setUserId(cashOutApply.getUserId());
    wechatCashoutLog.setCashoutApplyId(cashOutApply.getId());
    wechatCashoutLog.setRequestParams(weChatCashOut.toString());
    wechatCashoutLog.setCashoutApplyNo(cashOutApply.getCashoutNo());
    //DecimalUtils.yzf 金额转化 / 元转分 / 保留两位小数
    wechatCashoutLog.setCashoutAmount(DecimalUtils.yzf(weChatCashOut.getAmount()).longValue());
    wechatCashoutLog.setPaymentNo(resultMap.get("payment_no"));
    logger.info("响应支付时间" + resultMap.get("payment_time"));
    wechatCashoutLog.setResultCode(resultMap.get("result_code"));
    wechatCashoutLog.setReturnMsg(resultMap.get("return_msg"));
    wechatCashoutLog.setReturnCode(resultMap.get("return_code"));
    wechatCashoutLog.setReturnInfo(new com.alibaba.fastjson2.JSONObject(resultMap).toString());
    wechatCashOutLogService.insertWechatCashoutLog(wechatCashoutLog);
}

7.4 判断是否提现失败,提现失败直接回滚,进行提现中,展示在后台提现列表进行处理

    //提现失败回滚
  if (!WxPayStatus.SUCCESS.name().equals(resultMap.get(WxPayResultStatus.RESULT_CODE.getVal()))) {
        throw new ServiceException("微信提现失败,请稍后重试");
    }
 -------------------------------------------------------------------------------------------------------------------------------
【WxPayResultStatus】(微信支付结果状态--枚举类)
public enum WxPayResultStatus {
RETURN_CODE("return_code","响应码"),

RESULT_CODE("result_code","结果码"),

ERR_CODE_DES("err_code_des","错误信息"),

ERR_CODE("err_code","错误码"),
;
WxPayResultStatus(String val,String msg) {
    this.val = val;
    this.msg = msg;
}

public String getMsg() {
    return msg;
}
public String getVal() {
    return val;
}

private final String msg;

private final String val;

}

到这里小程序提现就结束了!

如果提现失败,可以在后台再次操作提现!
后台提下实现就很简单了,跟小程序提现一样组装数据
因为提现记录里面存的有提现人的所有信息,比如用户ID,提现单号,openID,提现金额,手续费等.
只需要查询出来,根据自己的业务场景效验下,组装好数据再此调用微信提现方法就可以了.

提现申请表:
请添加图片描述企业付款零钱请求表:
请添加图片描述后台提现记录页面:
请添加图片描述

以上就是全部内容,欢迎大家指导交流! bye!

  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值