package com.izouma.nineth.service; import com.alibaba.fastjson.JSONObject; import com.izouma.nineth.annotations.RedisLock; import com.izouma.nineth.config.Constants; import com.izouma.nineth.domain.WithdrawApply; import com.izouma.nineth.dto.PageQuery; import com.izouma.nineth.dto.UserBankCard; import com.izouma.nineth.enums.BalanceType; import com.izouma.nineth.enums.WithdrawStatus; import com.izouma.nineth.exception.BusinessException; import com.izouma.nineth.repo.BalanceRecordRepo; import com.izouma.nineth.repo.UserBalanceRepo; import com.izouma.nineth.repo.UserBankCardRepo; import com.izouma.nineth.repo.WithdrawApplyRepo; import com.izouma.nineth.utils.JpaUtils; import com.izouma.nineth.utils.SnowflakeIdWorker; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.redisson.api.RedissonClient; import org.springframework.core.env.Environment; import org.springframework.data.domain.Page; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Arrays; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @Service @AllArgsConstructor @Slf4j public class WithdrawApplyService { private WithdrawApplyRepo withdrawApplyRepo; private UserBalanceService userBalanceService; private UserBalanceRepo userBalanceRepo; private BalanceRecordRepo balanceRecordRepo; private SysConfigService sysConfigService; private SandPayService sandPayService; private UserBankCardRepo userBankCardRepo; private SnowflakeIdWorker snowflakeIdWorker; private Environment env; private RedissonClient redissonClient; private PayEaseService payEaseService; public Page all(PageQuery pageQuery) { return withdrawApplyRepo.findAll(JpaUtils.toSpecification(pageQuery, WithdrawApply.class), JpaUtils.toPageRequest(pageQuery)); } @RedisLock("'withdrawApply'+#userId") public WithdrawApply apply(Long userId, BigDecimal amount) { if (!sysConfigService.getBoolean("enable_withdraw")) { throw new BusinessException("提现功能暂时关闭"); } int limit = sysConfigService.getInt("daily_withdraw_limit"); if (withdrawApplyRepo.countByUserIdAndCreatedAtBetween(userId, LocalDate.now().atStartOfDay(), LocalDate.now().atTime(LocalTime.MAX)) >= limit) { throw new BusinessException("每天只能申请提现" + limit + "次"); } userBalanceService.preWithdraw(userId, amount); WithdrawApply withdrawApply = WithdrawApply.builder() .userId(userId) .amount(amount) .status(WithdrawStatus.PENDING) .build(); return withdrawApplyRepo.save(withdrawApply); } @RedisLock("'finishWithdrawApply'+#id") public WithdrawApply finishWithdrawApply(Long id, boolean approve, String reason) { String channel = sysConfigService.getString("withdraw_channel"); if (!(Constants.PayChannel.SAND.equals(channel) || Constants.PayChannel.PE.equals(channel))) { throw new BusinessException("unsupported channel"); } WithdrawApply apply = withdrawApplyRepo.findById(id).orElseThrow(new BusinessException("提现申请不存在")); if (apply.getStatus() != WithdrawStatus.PENDING) { throw new BusinessException("提现申请已处理"); } if (approve) { UserBankCard bankCard = userBankCardRepo.findByUserId(apply.getUserId()) .stream().findFirst() .orElse(null); if (bankCard == null) { apply.setStatus(WithdrawStatus.FAIL); apply.setFinishTime(LocalDateTime.now()); apply.setReason(Optional.ofNullable(reason).orElse("用户未绑卡")); userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.DENY, "用户未绑卡", false, null); } else { BigDecimal chargeAmount; String withdrawCharge = sysConfigService.getString("withdraw_charge"); if (StringUtils.isNotEmpty(withdrawCharge) && Pattern.matches("^(\\d+|\\d+.\\d+),\\d+$", withdrawCharge)) { String[] arr = withdrawCharge.split(","); chargeAmount = apply.getAmount().multiply(new BigDecimal(arr[0])) .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); BigDecimal minChargeAmount = new BigDecimal(arr[1]); if (chargeAmount.compareTo(minChargeAmount) < 0) { chargeAmount = minChargeAmount; } } else { chargeAmount = BigDecimal.ZERO; } String withdrawId = snowflakeIdWorker.nextId() + ""; try { String msg = ""; boolean success = false; if (Arrays.asList(env.getActiveProfiles()).contains("prod")) { try { if (Constants.PayChannel.SAND.equals(channel)) { JSONObject res = sandPayService.transfer(withdrawId, bankCard.getRealName(), bankCard.getBankNo(), apply.getAmount().subtract(chargeAmount), bankCard.getPhone()); if ("0000".equals(res.getString("respCode"))) { success = true; } else { msg = res.getString("respDesc"); } } else { payEaseService.transfer(withdrawId, bankCard.getRealName(), bankCard.getBankNo(), apply.getAmount().subtract(chargeAmount), bankCard.getPhone()); success = true; } } catch (Exception e) { log.error("提现出错", e); msg = e.getMessage(); } if (!success) { throw new BusinessException(msg); } apply.setStatus(WithdrawStatus.SUCCESS); apply.setFinishTime(LocalDateTime.now()); apply.setWithdrawId(withdrawId); apply.setChannel(Constants.PayChannel.SAND); } else { if (Math.random() > 0.5) { success = true; apply.setStatus(WithdrawStatus.SUCCESS); apply.setFinishTime(LocalDateTime.now()); apply.setWithdrawId(withdrawId); apply.setChannel(Constants.PayChannel.SAND); } else { throw new BusinessException("测试服随机失败"); } } } catch (Exception e) { if (e.getMessage() == null || "商户IP不合法".equals(e.getMessage()) || "账户余额不足".equals(e.getMessage()) || e.getMessage().contains("系统繁忙")) { log.info("提现失败", e); apply.setReason(e.getMessage()); } else { apply.setStatus(WithdrawStatus.FAIL); apply.setFinishTime(LocalDateTime.now()); apply.setReason(e.getMessage()); boolean lock = e.getMessage() == null || !e.getMessage().contains("系统繁忙"); userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.RETURN, e.getMessage(), lock, withdrawId); } } } } else { apply.setStatus(WithdrawStatus.FAIL); apply.setFinishTime(LocalDateTime.now()); apply.setReason(Optional.ofNullable(reason).orElse("审核不通过")); userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.DENY, apply.getReason(), false, null); } return withdrawApplyRepo.save(apply); } @Async @RedisLock(value = "'approveAll'", expire = 1, unit = TimeUnit.HOURS) public void approveAllAsync() throws ExecutionException, InterruptedException { approveAll(); } @RedisLock(value = "'approveAll'", expire = 1, unit = TimeUnit.HOURS) public void approveAll() throws ExecutionException, InterruptedException { new ForkJoinPool(5).submit(() -> { withdrawApplyRepo.findByCreatedAtBeforeAndStatus(LocalDate.now().atStartOfDay(), WithdrawStatus.PENDING) .parallelStream().forEach(withdrawApply -> { try { finishWithdrawApply(withdrawApply.getId(), true, null); } catch (Exception ignored) { } }); }).get(); } @Async @RedisLock(value = "'applyAll'", expire = 1, unit = TimeUnit.HOURS) public void applyAll() throws ExecutionException, InterruptedException { new ForkJoinPool(5).submit(() -> { userBalanceRepo.findAll().parallelStream().forEach(userBalance -> { LocalDateTime time = LocalDateTime.now().minusDays(14); if (userBalance.getCreatedAt() != null && userBalance.getCreatedAt().isBefore(time) && userBalance.getBalance().compareTo(new BigDecimal("100")) >= 0 && withdrawApplyRepo.countByUserIdAndCreatedAtAfter(userBalance.getUserId(), time) <= 0) { log.info("apply withdraw for user {}", userBalance.getUserId()); try { apply(userBalance.getUserId(), userBalance.getBalance()); } catch (Exception e) { log.error("apply withdraw for user {}", userBalance.getUserId(), e); } } }); }).get(); } }