WithdrawApplyService.java 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. package com.izouma.nineth.service;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.izouma.nineth.annotations.RedisLock;
  4. import com.izouma.nineth.config.Constants;
  5. import com.izouma.nineth.domain.WithdrawApply;
  6. import com.izouma.nineth.dto.PageQuery;
  7. import com.izouma.nineth.dto.UserBankCard;
  8. import com.izouma.nineth.enums.BalanceType;
  9. import com.izouma.nineth.enums.WithdrawStatus;
  10. import com.izouma.nineth.exception.BusinessException;
  11. import com.izouma.nineth.repo.BalanceRecordRepo;
  12. import com.izouma.nineth.repo.UserBalanceRepo;
  13. import com.izouma.nineth.repo.UserBankCardRepo;
  14. import com.izouma.nineth.repo.WithdrawApplyRepo;
  15. import com.izouma.nineth.utils.JpaUtils;
  16. import com.izouma.nineth.utils.SnowflakeIdWorker;
  17. import lombok.AllArgsConstructor;
  18. import lombok.extern.slf4j.Slf4j;
  19. import org.apache.commons.lang3.StringUtils;
  20. import org.redisson.api.RedissonClient;
  21. import org.springframework.core.env.Environment;
  22. import org.springframework.data.domain.Page;
  23. import org.springframework.scheduling.annotation.Async;
  24. import org.springframework.stereotype.Service;
  25. import java.math.BigDecimal;
  26. import java.math.RoundingMode;
  27. import java.time.LocalDate;
  28. import java.time.LocalDateTime;
  29. import java.time.LocalTime;
  30. import java.util.Arrays;
  31. import java.util.Optional;
  32. import java.util.concurrent.ExecutionException;
  33. import java.util.concurrent.ForkJoinPool;
  34. import java.util.concurrent.TimeUnit;
  35. import java.util.regex.Pattern;
  36. @Service
  37. @AllArgsConstructor
  38. @Slf4j
  39. public class WithdrawApplyService {
  40. private WithdrawApplyRepo withdrawApplyRepo;
  41. private UserBalanceService userBalanceService;
  42. private UserBalanceRepo userBalanceRepo;
  43. private BalanceRecordRepo balanceRecordRepo;
  44. private SysConfigService sysConfigService;
  45. private SandPayService sandPayService;
  46. private UserBankCardRepo userBankCardRepo;
  47. private SnowflakeIdWorker snowflakeIdWorker;
  48. private Environment env;
  49. private RedissonClient redissonClient;
  50. private PayEaseService payEaseService;
  51. public Page<WithdrawApply> all(PageQuery pageQuery) {
  52. return withdrawApplyRepo.findAll(JpaUtils.toSpecification(pageQuery, WithdrawApply.class), JpaUtils.toPageRequest(pageQuery));
  53. }
  54. @RedisLock("'withdrawApply'+#userId")
  55. public WithdrawApply apply(Long userId, BigDecimal amount) {
  56. if (!sysConfigService.getBoolean("enable_withdraw")) {
  57. throw new BusinessException("提现功能暂时关闭");
  58. }
  59. int limit = sysConfigService.getInt("daily_withdraw_limit");
  60. if (withdrawApplyRepo.countByUserIdAndCreatedAtBetween(userId,
  61. LocalDate.now().atStartOfDay(), LocalDate.now().atTime(LocalTime.MAX)) >= limit) {
  62. throw new BusinessException("每天只能申请提现" + limit + "次");
  63. }
  64. userBalanceService.preWithdraw(userId, amount);
  65. WithdrawApply withdrawApply = WithdrawApply.builder()
  66. .userId(userId)
  67. .amount(amount)
  68. .status(WithdrawStatus.PENDING)
  69. .build();
  70. return withdrawApplyRepo.save(withdrawApply);
  71. }
  72. @RedisLock("'finishWithdrawApply'+#id")
  73. public WithdrawApply finishWithdrawApply(Long id, boolean approve, String reason) {
  74. String channel = sysConfigService.getString("withdraw_channel");
  75. if (!(Constants.PayChannel.SAND.equals(channel) || Constants.PayChannel.PE.equals(channel))) {
  76. throw new BusinessException("unsupported channel");
  77. }
  78. WithdrawApply apply = withdrawApplyRepo.findById(id).orElseThrow(new BusinessException("提现申请不存在"));
  79. if (apply.getStatus() != WithdrawStatus.PENDING) {
  80. throw new BusinessException("提现申请已处理");
  81. }
  82. if (approve) {
  83. UserBankCard bankCard = userBankCardRepo.findByUserId(apply.getUserId())
  84. .stream().findFirst()
  85. .orElse(null);
  86. if (bankCard == null) {
  87. apply.setStatus(WithdrawStatus.FAIL);
  88. apply.setFinishTime(LocalDateTime.now());
  89. apply.setReason(Optional.ofNullable(reason).orElse("用户未绑卡"));
  90. userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.DENY,
  91. "用户未绑卡", false, null);
  92. } else {
  93. BigDecimal chargeAmount;
  94. String withdrawCharge = sysConfigService.getString("withdraw_charge");
  95. if (StringUtils.isNotEmpty(withdrawCharge) && Pattern.matches("^(\\d+|\\d+.\\d+),\\d+$", withdrawCharge)) {
  96. String[] arr = withdrawCharge.split(",");
  97. chargeAmount = apply.getAmount().multiply(new BigDecimal(arr[0]))
  98. .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
  99. BigDecimal minChargeAmount = new BigDecimal(arr[1]);
  100. if (chargeAmount.compareTo(minChargeAmount) < 0) {
  101. chargeAmount = minChargeAmount;
  102. }
  103. } else {
  104. chargeAmount = BigDecimal.ZERO;
  105. }
  106. String withdrawId = snowflakeIdWorker.nextId() + "";
  107. try {
  108. String msg = "";
  109. boolean success = false;
  110. if (Arrays.asList(env.getActiveProfiles()).contains("prod")) {
  111. try {
  112. if (Constants.PayChannel.SAND.equals(channel)) {
  113. JSONObject res = sandPayService.transfer(withdrawId, bankCard.getRealName(), bankCard.getBankNo(),
  114. apply.getAmount().subtract(chargeAmount), bankCard.getPhone());
  115. if ("0000".equals(res.getString("respCode"))) {
  116. success = true;
  117. } else {
  118. msg = res.getString("respDesc");
  119. }
  120. } else {
  121. payEaseService.transfer(withdrawId, bankCard.getRealName(), bankCard.getBankNo(),
  122. apply.getAmount().subtract(chargeAmount), bankCard.getPhone());
  123. success = true;
  124. }
  125. } catch (Exception e) {
  126. log.error("提现出错", e);
  127. msg = e.getMessage();
  128. }
  129. if (!success) {
  130. throw new BusinessException(msg);
  131. }
  132. apply.setStatus(WithdrawStatus.SUCCESS);
  133. apply.setFinishTime(LocalDateTime.now());
  134. apply.setWithdrawId(withdrawId);
  135. apply.setChannel(Constants.PayChannel.SAND);
  136. } else {
  137. if (Math.random() > 0.5) {
  138. success = true;
  139. apply.setStatus(WithdrawStatus.SUCCESS);
  140. apply.setFinishTime(LocalDateTime.now());
  141. apply.setWithdrawId(withdrawId);
  142. apply.setChannel(Constants.PayChannel.SAND);
  143. } else {
  144. throw new BusinessException("测试服随机失败");
  145. }
  146. }
  147. } catch (Exception e) {
  148. if (e.getMessage() == null ||
  149. "商户IP不合法".equals(e.getMessage()) ||
  150. "账户余额不足".equals(e.getMessage()) ||
  151. e.getMessage().contains("系统繁忙")) {
  152. log.info("提现失败", e);
  153. apply.setReason(e.getMessage());
  154. } else {
  155. apply.setStatus(WithdrawStatus.FAIL);
  156. apply.setFinishTime(LocalDateTime.now());
  157. apply.setReason(e.getMessage());
  158. boolean lock = e.getMessage() == null || !e.getMessage().contains("系统繁忙");
  159. userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.RETURN,
  160. e.getMessage(), lock, withdrawId);
  161. }
  162. }
  163. }
  164. } else {
  165. apply.setStatus(WithdrawStatus.FAIL);
  166. apply.setFinishTime(LocalDateTime.now());
  167. apply.setReason(Optional.ofNullable(reason).orElse("审核不通过"));
  168. userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.DENY,
  169. apply.getReason(), false, null);
  170. }
  171. return withdrawApplyRepo.save(apply);
  172. }
  173. @Async
  174. @RedisLock(value = "'approveAll'", expire = 1, unit = TimeUnit.HOURS)
  175. public void approveAllAsync() throws ExecutionException, InterruptedException {
  176. approveAll();
  177. }
  178. @RedisLock(value = "'approveAll'", expire = 1, unit = TimeUnit.HOURS)
  179. public void approveAll() throws ExecutionException, InterruptedException {
  180. new ForkJoinPool(5).submit(() -> {
  181. withdrawApplyRepo.findByCreatedAtBeforeAndStatus(LocalDate.now().atStartOfDay(),
  182. WithdrawStatus.PENDING)
  183. .parallelStream().forEach(withdrawApply -> {
  184. try {
  185. finishWithdrawApply(withdrawApply.getId(), true, null);
  186. } catch (Exception ignored) {
  187. }
  188. });
  189. }).get();
  190. }
  191. @Async
  192. @RedisLock(value = "'applyAll'", expire = 1, unit = TimeUnit.HOURS)
  193. public void applyAll() throws ExecutionException, InterruptedException {
  194. new ForkJoinPool(5).submit(() -> {
  195. userBalanceRepo.findAll().parallelStream().forEach(userBalance -> {
  196. LocalDateTime time = LocalDateTime.now().minusDays(14);
  197. if (userBalance.getCreatedAt() != null && userBalance.getCreatedAt().isBefore(time)
  198. && userBalance.getBalance().compareTo(new BigDecimal("100")) >= 0
  199. && withdrawApplyRepo.countByUserIdAndCreatedAtAfter(userBalance.getUserId(), time) <= 0) {
  200. log.info("apply withdraw for user {}", userBalance.getUserId());
  201. try {
  202. apply(userBalance.getUserId(), userBalance.getBalance());
  203. } catch (Exception e) {
  204. log.error("apply withdraw for user {}", userBalance.getUserId(), e);
  205. }
  206. }
  207. });
  208. }).get();
  209. }
  210. }