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.3 【调用提现接口】
//调用提现接口
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,提现金额,手续费等.
只需要查询出来,根据自己的业务场景效验下,组装好数据再此调用微信提现方法就可以了.
企业付款零钱请求表:
后台提现记录页面: