UserBalanceService.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. package com.izouma.nineth.service;
  2. import cn.hutool.core.util.ZipUtil;
  3. import com.alibaba.excel.EasyExcel;
  4. import com.alibaba.fastjson.JSONObject;
  5. import com.izouma.nineth.domain.*;
  6. import com.izouma.nineth.dto.SandPaySettle;
  7. import com.izouma.nineth.dto.UserBankCard;
  8. import com.izouma.nineth.dto.UserWithdraw;
  9. import com.izouma.nineth.enums.BalanceType;
  10. import com.izouma.nineth.enums.CollectionSource;
  11. import com.izouma.nineth.enums.OrderStatus;
  12. import com.izouma.nineth.exception.BusinessException;
  13. import com.izouma.nineth.repo.*;
  14. import com.izouma.nineth.service.storage.StorageService;
  15. import com.izouma.nineth.utils.DateTimeUtils;
  16. import com.izouma.nineth.utils.SnowflakeIdWorker;
  17. import lombok.AllArgsConstructor;
  18. import lombok.extern.slf4j.Slf4j;
  19. import org.apache.commons.lang3.RandomStringUtils;
  20. import org.apache.commons.lang3.StringUtils;
  21. import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
  22. import org.apache.poi.ss.usermodel.Row;
  23. import org.apache.poi.ss.usermodel.Sheet;
  24. import org.apache.poi.ss.usermodel.Workbook;
  25. import org.apache.poi.ss.usermodel.WorkbookFactory;
  26. import org.apache.poi.util.TempFile;
  27. import org.springframework.data.redis.core.RedisTemplate;
  28. import org.springframework.scheduling.annotation.Async;
  29. import org.springframework.stereotype.Service;
  30. import org.springframework.web.multipart.MultipartFile;
  31. import javax.transaction.Transactional;
  32. import java.io.*;
  33. import java.math.BigDecimal;
  34. import java.math.RoundingMode;
  35. import java.time.Duration;
  36. import java.time.LocalDate;
  37. import java.time.LocalDateTime;
  38. import java.time.LocalTime;
  39. import java.time.temporal.ChronoUnit;
  40. import java.util.*;
  41. import java.util.regex.Pattern;
  42. import java.util.stream.Collectors;
  43. @Service
  44. @Slf4j
  45. @AllArgsConstructor
  46. public class UserBalanceService {
  47. private final UserBalanceRepo userBalanceRepo;
  48. private final BalanceRecordRepo balanceRecordRepo;
  49. private final OrderRepo orderRepo;
  50. private final AssetRepo assetRepo;
  51. private final UserBankCardRepo userBankCardRepo;
  52. private final SettleRecordRepo settleRecordRepo;
  53. private final StorageService storageService;
  54. private final ExportWithdrawRepo exportWithdrawRepo;
  55. private final AutoWithdrawRecordRepo autoWithdrawRecordRepo;
  56. private final SnowflakeIdWorker snowflakeIdWorker;
  57. private final SandPayService sandPayService;
  58. private final RedisTemplate<String, Object> redisTemplate;
  59. public void settle(LocalDate start, LocalDate end) {
  60. for (long i = 0; i <= ChronoUnit.DAYS.between(start, end); i++) {
  61. LocalDate date = start.plusDays(i);
  62. if (settleRecordRepo.findByDate(date).isPresent()) {
  63. throw new BusinessException(DateTimeUtils.format(date, "yyyy-MM-dd") + "已经结算过");
  64. }
  65. }
  66. for (long i = 0; i <= ChronoUnit.DAYS.between(start, end); i++) {
  67. LocalDate date = start.plusDays(i);
  68. settle(date);
  69. }
  70. }
  71. public void settle(LocalDate date) {
  72. if (settleRecordRepo.findByDate(date).isPresent()) {
  73. throw new BusinessException(DateTimeUtils.format(date, "yyyy-MM-dd") + "已经结算过");
  74. }
  75. List<Order> orders = orderRepo.findByCreatedAtBetweenAndSourceAndStatusIn(date.atStartOfDay(),
  76. date.atTime(23, 59, 59, 9999), CollectionSource.TRANSFER,
  77. Arrays.asList(OrderStatus.PROCESSING, OrderStatus.FINISH));
  78. List<Asset> assets = assetRepo.findAllById(orders.stream().map(Order::getAssetId).collect(Collectors.toSet()));
  79. BigDecimal totalAmount = BigDecimal.ZERO;
  80. BigDecimal royaltiesAmount = BigDecimal.ZERO;
  81. BigDecimal serviceChargeAmount = BigDecimal.ZERO;
  82. List<UserBalance> balanceList = new ArrayList<>();
  83. List<BalanceRecord> recordList = new ArrayList<>();
  84. int c = 0;
  85. for (Order order : orders) {
  86. log.info("结算订单 {}/{}, orderId={}", ++c, orders.size(), order.getId());
  87. Asset asset = assets.stream().filter(i -> i.getId().equals(order.getAssetId()))
  88. .findFirst()
  89. .orElseThrow(new BusinessException("藏品不存在"));
  90. UserBalance userBalance = balanceList.stream().filter(b -> b.getUserId().equals(asset.getUserId()))
  91. .findFirst().orElse(null);
  92. if (userBalance == null) {
  93. userBalance = userBalanceRepo.findById(asset.getUserId())
  94. .orElse(new UserBalance(asset.getUserId(), BigDecimal.ZERO, BigDecimal.ZERO,
  95. false, null, null));
  96. balanceList.add(userBalance);
  97. }
  98. BigDecimal amount = order.getTotalPrice()
  99. .subtract(order.getGasPrice())
  100. .multiply(BigDecimal.valueOf(100 - order.getRoyalties() - order.getServiceCharge()))
  101. .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
  102. totalAmount = totalAmount.add(order.getTotalPrice());
  103. royaltiesAmount = royaltiesAmount.add(order.getTotalPrice()
  104. .subtract(order.getGasPrice())
  105. .multiply(BigDecimal.valueOf(order.getRoyalties()))
  106. .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP));
  107. serviceChargeAmount = serviceChargeAmount.add(order.getTotalPrice()
  108. .subtract(order.getGasPrice())
  109. .multiply(BigDecimal.valueOf(order.getServiceCharge()))
  110. .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP));
  111. userBalance.setLastBalance(userBalance.getBalance());
  112. userBalance.setBalance(userBalance.getBalance().add(amount));
  113. recordList.add(BalanceRecord.builder()
  114. .time(LocalDateTime.now())
  115. .userId(asset.getUserId())
  116. .orderId(order.getId())
  117. .amount(amount)
  118. .balance(userBalance.getBalance())
  119. .lastBalance(userBalance.getLastBalance())
  120. .type(BalanceType.SELL)
  121. .build());
  122. }
  123. userBalanceRepo.saveAll(balanceList);
  124. balanceRecordRepo.saveAll(recordList);
  125. settleRecordRepo.save(new SettleRecord(date, orders.size(), totalAmount, royaltiesAmount, serviceChargeAmount));
  126. }
  127. @Async
  128. @Transactional
  129. public ExportWithdraw exportWithdrawAsync(String remark) throws IOException, InvalidFormatException {
  130. return exportWithdraw(remark);
  131. }
  132. @Transactional
  133. public ExportWithdraw exportWithdraw(String remark) throws IOException, InvalidFormatException {
  134. List<UserBalance> balanceList = userBalanceRepo.findByBalanceGreaterThan(BigDecimal.ZERO);
  135. List<UserBankCard> userBankCardList = balanceList.isEmpty() ? new ArrayList<>() :
  136. userBankCardRepo.findByUserIdIn(balanceList.stream().map(UserBalance::getUserId)
  137. .collect(Collectors.toSet()));
  138. List<UserWithdraw> withdrawList = new ArrayList<>();
  139. Iterator<UserBalance> it = balanceList.iterator();
  140. UserBalance ub;
  141. while (it.hasNext()) {
  142. ub = it.next();
  143. Long userId = ub.getUserId();
  144. log.info("查询提现银行卡userId={}", ub.getUserId());
  145. UserBankCard ubc = userBankCardList.stream().filter(u -> u.getUserId().equals(userId))
  146. .findFirst().orElse(null);
  147. if (ubc != null) {
  148. withdrawList.add(new UserWithdraw(userId, ubc.getRealName(), ubc.getBankNo(), ub.getBalance()));
  149. } else {
  150. it.remove();
  151. }
  152. }
  153. File tmpDir = TempFile.createTempDirectory("export_" + RandomStringUtils.randomAlphabetic(8));
  154. File file1 = new File(tmpDir, DateTimeUtils.format(LocalDate.now(), "yyyyMMdd") + "结算.xlsx");
  155. File file2 = new File(tmpDir, DateTimeUtils.format(LocalDate.now(), "yyyyMMdd") + "结算导入.xls");
  156. EasyExcel.write(file1, UserWithdraw.class)
  157. .sheet("sheet").doWrite(withdrawList);
  158. InputStream inputStream = getClass().getResourceAsStream("/批量付款到对私银行账户模板.xls");
  159. Workbook workbook = WorkbookFactory.create(inputStream);
  160. Sheet sheet = workbook.getSheetAt(0);
  161. for (int i = 0; i < withdrawList.size(); i++) {
  162. Row row = Optional.ofNullable(sheet.getRow(i + 1)).orElse(sheet.createRow(i + 1));
  163. Optional.ofNullable(row.getCell(1))
  164. .orElse(row.createCell(1))
  165. .setCellValue(withdrawList.get(i).getName());
  166. Optional.ofNullable(row.getCell(2))
  167. .orElse(row.createCell(2))
  168. .setCellValue(withdrawList.get(i).getBankNo());
  169. Optional.ofNullable(row.getCell(3))
  170. .orElse(row.createCell(3))
  171. .setCellValue(withdrawList.get(i).getAmount().doubleValue());
  172. Optional.ofNullable(row.getCell(4))
  173. .orElse(row.createCell(4))
  174. .setCellValue(withdrawList.get(i).getUserId());
  175. }
  176. inputStream.close();
  177. FileOutputStream os = new FileOutputStream(file2);
  178. workbook.write(os);
  179. workbook.close();
  180. File zipFile = ZipUtil.zip(tmpDir);
  181. String url = storageService.uploadFromInputStream(new FileInputStream(zipFile),
  182. "upload/" + DateTimeUtils.format(LocalDateTime.now(), "yyyyMMddHHmm") + "_" + RandomStringUtils.randomNumeric(8) + ".zip");
  183. ExportWithdraw exportWithdraw = exportWithdrawRepo.save(new ExportWithdraw(url, remark, withdrawList.size(),
  184. withdrawList.stream().map(UserWithdraw::getAmount).reduce(BigDecimal::add)
  185. .orElse(BigDecimal.ZERO), "处理中"));
  186. balanceList.parallelStream().forEach(userBalance -> {
  187. log.info("提现userId={}", userBalance.getUserId());
  188. BigDecimal amount = userBalance.getBalance();
  189. userBalance.setLastBalance(userBalance.getBalance());
  190. userBalance.setBalance(BigDecimal.ZERO);
  191. userBalanceRepo.saveAndFlush(userBalance);
  192. balanceRecordRepo.save(BalanceRecord.builder()
  193. .time(LocalDateTime.now())
  194. .userId(userBalance.getUserId())
  195. .amount(amount.negate())
  196. .balance(BigDecimal.ZERO)
  197. .lastBalance(userBalance.getLastBalance())
  198. .type(BalanceType.WITHDRAW)
  199. .build());
  200. });
  201. tmpDir.delete();
  202. zipFile.delete();
  203. return exportWithdraw;
  204. }
  205. @Transactional
  206. public void importFail(MultipartFile withdrawFile, MultipartFile settleFile) throws IOException {
  207. List<SandPaySettle> failSettleList = EasyExcel.read(settleFile.getInputStream())
  208. .head(SandPaySettle.class).sheet()
  209. .doReadSync();
  210. failSettleList = failSettleList.stream()
  211. .filter(s -> "失败".equals(s.getStatus().trim()))
  212. .collect(Collectors.toList());
  213. List<UserWithdraw> withdrawList = EasyExcel.read(withdrawFile.getInputStream())
  214. .head(UserWithdraw.class).sheet()
  215. .doReadSync();
  216. List<UserWithdraw> failWithdraw = new ArrayList<>();
  217. for (SandPaySettle sandPaySettle : failSettleList) {
  218. List<UserWithdraw> list;
  219. if (StringUtils.isNotBlank(sandPaySettle.getRemark()) && Pattern.matches("^\\d+$", sandPaySettle.getRemark()
  220. .trim())) {
  221. Long userId = Long.parseLong(sandPaySettle.getRemark().trim());
  222. list = withdrawList.stream().filter(i -> i.getUserId().equals(userId))
  223. .collect(Collectors.toList());
  224. } else {
  225. list = withdrawList.stream().filter(i -> i.getBankNo().equals(sandPaySettle.getBankNo())
  226. && i.getName().equals(sandPaySettle.getName())
  227. && i.getAmount().compareTo(sandPaySettle.getAmount()) == 0)
  228. .collect(Collectors.toList());
  229. }
  230. if (list.size() != 1) {
  231. throw new BusinessException("不唯一:" + sandPaySettle.getName() + "," + sandPaySettle.getBankNo());
  232. } else {
  233. failWithdraw.add(list.get(0));
  234. }
  235. }
  236. failWithdraw.parallelStream().forEach(withdraw -> {
  237. UserBalance userBalance = userBalanceRepo.findById(withdraw.getUserId())
  238. .orElse(new UserBalance(withdraw.getUserId(), BigDecimal.ZERO, BigDecimal.ZERO,
  239. false, null, null));
  240. userBalance.setLastBalance(userBalance.getBalance());
  241. userBalance.setBalance(userBalance.getBalance().add(withdraw.getAmount()));
  242. userBalanceRepo.saveAndFlush(userBalance);
  243. balanceRecordRepo.save(BalanceRecord.builder()
  244. .time(LocalDateTime.now())
  245. .userId(userBalance.getUserId())
  246. .amount(withdraw.getAmount())
  247. .balance(userBalance.getBalance())
  248. .lastBalance(userBalance.getLastBalance())
  249. .type(BalanceType.RETURN)
  250. .build());
  251. });
  252. }
  253. @Async
  254. public void autoWithdraw(LocalDate date) {
  255. autoWithdrawRecordRepo.findByDate(date).ifPresent(a -> {
  256. throw new BusinessException("今日已经提现过");
  257. });
  258. List<UserBalance> list = userBalanceRepo.findByLockedFalseAndBalanceGreaterThanOrderByUserId(BigDecimal.ZERO);
  259. AutoWithdrawRecord record = AutoWithdrawRecord.builder()
  260. .date(LocalDate.now())
  261. .status("pending")
  262. .progress(0)
  263. .total(list.size())
  264. .build();
  265. autoWithdrawRecordRepo.saveAndFlush(record);
  266. for (UserBalance userBalance : list) {
  267. UserBankCard userBankCard = userBankCardRepo.findByUserId(userBalance.getUserId())
  268. .stream().findFirst().orElse(null);
  269. if (userBankCard == null) {
  270. log.info("自动提现userId={}, amount={}, 未绑卡", userBalance.getBalance(), userBalance.getUserId());
  271. record.setProgress(record.getProgress() + 1);
  272. record.setCurrentUserId(userBalance.getUserId());
  273. autoWithdrawRecordRepo.saveAndFlush(record);
  274. } else {
  275. log.info("自动提现userId={}, amount={}, name={}, bank={}",
  276. userBalance.getUserId(), userBalance.getBalance(),
  277. userBankCard.getRealName(), userBankCard.getBankNo());
  278. String withdrawId = snowflakeIdWorker.nextId() + "";
  279. BigDecimal amount = userBalance.getBalance();
  280. userBalance.setLastBalance(userBalance.getBalance());
  281. userBalance.setBalance(BigDecimal.ZERO);
  282. userBalanceRepo.saveAndFlush(userBalance);
  283. balanceRecordRepo.save(BalanceRecord.builder()
  284. .time(LocalDateTime.now())
  285. .userId(userBalance.getUserId())
  286. .amount(amount.negate())
  287. .balance(BigDecimal.ZERO)
  288. .lastBalance(userBalance.getLastBalance())
  289. .type(BalanceType.WITHDRAW)
  290. .withdrawId(withdrawId)
  291. .build());
  292. boolean success = false;
  293. String msg = null;
  294. try {
  295. JSONObject res = sandPayService.transfer(withdrawId, userBankCard.getRealName(), userBankCard.getBankNo(), amount);
  296. if ("0000".equals(res.getString("respCode"))) {
  297. success = true;
  298. } else {
  299. msg = res.getString("respDesc");
  300. }
  301. } catch (Exception e) {
  302. msg = e.getMessage();
  303. }
  304. if (!success) {
  305. userBalance.setLastBalance(userBalance.getBalance());
  306. userBalance.setBalance(userBalance.getBalance().add(amount));
  307. userBalanceRepo.saveAndFlush(userBalance);
  308. balanceRecordRepo.save(BalanceRecord.builder()
  309. .time(LocalDateTime.now())
  310. .userId(userBalance.getUserId())
  311. .amount(amount)
  312. .balance(userBalance.getBalance())
  313. .lastBalance(userBalance.getLastBalance())
  314. .type(BalanceType.RETURN)
  315. .withdrawId(withdrawId)
  316. .remark(msg)
  317. .build());
  318. }
  319. record.setProgress(record.getProgress() + 1);
  320. record.setCurrentUserId(userBalance.getUserId());
  321. autoWithdrawRecordRepo.saveAndFlush(record);
  322. if (!success) {
  323. msg = msg == null ? "" : msg;
  324. if (msg.contains("风控") || Pattern.matches(".*\\[.*\\]", msg)) {
  325. userBalance.setLocked(true);
  326. userBalance.setLockReason(msg);
  327. userBalance.setLockTime(LocalDateTime.now());
  328. }
  329. }
  330. }
  331. }
  332. record.setStatus("finish");
  333. autoWithdrawRecordRepo.saveAndFlush(record);
  334. redisTemplate.delete("autoWithdraw::" + DateTimeUtils.format(date, "yyyyMMdd"));
  335. }
  336. }