package com.izouma.nineth.service; import cn.hutool.core.util.ZipUtil; import com.alibaba.excel.EasyExcel; import com.alibaba.fastjson.JSONObject; import com.izouma.nineth.domain.*; import com.izouma.nineth.dto.SandPaySettle; import com.izouma.nineth.dto.UserBankCard; import com.izouma.nineth.dto.UserWithdraw; import com.izouma.nineth.enums.BalanceType; import com.izouma.nineth.enums.CollectionSource; import com.izouma.nineth.enums.OrderStatus; import com.izouma.nineth.exception.BusinessException; import com.izouma.nineth.repo.*; import com.izouma.nineth.service.storage.StorageService; import com.izouma.nineth.utils.DateTimeUtils; import com.izouma.nineth.utils.SnowflakeIdWorker; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.apache.poi.util.TempFile; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.transaction.Transactional; import java.io.*; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; @Service @Slf4j @AllArgsConstructor public class UserBalanceService { private final UserBalanceRepo userBalanceRepo; private final BalanceRecordRepo balanceRecordRepo; private final OrderRepo orderRepo; private final AssetRepo assetRepo; private final UserBankCardRepo userBankCardRepo; private final SettleRecordRepo settleRecordRepo; private final StorageService storageService; private final ExportWithdrawRepo exportWithdrawRepo; private final AutoWithdrawRecordRepo autoWithdrawRecordRepo; private final SnowflakeIdWorker snowflakeIdWorker; private final SandPayService sandPayService; private final RedisTemplate redisTemplate; public void settle(LocalDate start, LocalDate end) { for (long i = 0; i <= ChronoUnit.DAYS.between(start, end); i++) { LocalDate date = start.plusDays(i); if (settleRecordRepo.findByDate(date).isPresent()) { throw new BusinessException(DateTimeUtils.format(date, "yyyy-MM-dd") + "已经结算过"); } } for (long i = 0; i <= ChronoUnit.DAYS.between(start, end); i++) { LocalDate date = start.plusDays(i); settle(date); } } public void settle(LocalDate date) { if (settleRecordRepo.findByDate(date).isPresent()) { throw new BusinessException(DateTimeUtils.format(date, "yyyy-MM-dd") + "已经结算过"); } List orders = orderRepo.findByCreatedAtBetweenAndSourceAndStatusIn(date.atStartOfDay(), date.atTime(23, 59, 59, 9999), CollectionSource.TRANSFER, Arrays.asList(OrderStatus.PROCESSING, OrderStatus.FINISH)); List assets = assetRepo.findAllById(orders.stream().map(Order::getAssetId).collect(Collectors.toSet())); BigDecimal totalAmount = BigDecimal.ZERO; BigDecimal royaltiesAmount = BigDecimal.ZERO; BigDecimal serviceChargeAmount = BigDecimal.ZERO; List balanceList = new ArrayList<>(); List recordList = new ArrayList<>(); int c = 0; for (Order order : orders) { log.info("结算订单 {}/{}, orderId={}", ++c, orders.size(), order.getId()); Asset asset = assets.stream().filter(i -> i.getId().equals(order.getAssetId())) .findFirst() .orElseThrow(new BusinessException("藏品不存在")); UserBalance userBalance = balanceList.stream().filter(b -> b.getUserId().equals(asset.getUserId())) .findFirst().orElse(null); if (userBalance == null) { userBalance = userBalanceRepo.findById(asset.getUserId()) .orElse(new UserBalance(asset.getUserId(), BigDecimal.ZERO, BigDecimal.ZERO)); balanceList.add(userBalance); } BigDecimal amount = order.getTotalPrice() .subtract(order.getGasPrice()) .multiply(BigDecimal.valueOf(100 - order.getRoyalties() - order.getServiceCharge())) .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); totalAmount = totalAmount.add(order.getTotalPrice()); royaltiesAmount = royaltiesAmount.add(order.getTotalPrice() .subtract(order.getGasPrice()) .multiply(BigDecimal.valueOf(order.getRoyalties())) .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); serviceChargeAmount = serviceChargeAmount.add(order.getTotalPrice() .subtract(order.getGasPrice()) .multiply(BigDecimal.valueOf(order.getServiceCharge())) .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); userBalance.setLastBalance(userBalance.getBalance()); userBalance.setBalance(userBalance.getBalance().add(amount)); recordList.add(BalanceRecord.builder() .time(LocalDateTime.now()) .userId(asset.getUserId()) .orderId(order.getId()) .amount(amount) .balance(userBalance.getBalance()) .lastBalance(userBalance.getLastBalance()) .type(BalanceType.SELL) .build()); } userBalanceRepo.saveAll(balanceList); balanceRecordRepo.saveAll(recordList); settleRecordRepo.save(new SettleRecord(date, orders.size(), totalAmount, royaltiesAmount, serviceChargeAmount)); } @Async @Transactional public ExportWithdraw exportWithdrawAsync(String remark) throws IOException, InvalidFormatException { return exportWithdraw(remark); } @Transactional public ExportWithdraw exportWithdraw(String remark) throws IOException, InvalidFormatException { List balanceList = userBalanceRepo.findByBalanceGreaterThan(BigDecimal.ZERO); List userBankCardList = balanceList.isEmpty() ? new ArrayList<>() : userBankCardRepo.findByUserIdIn(balanceList.stream().map(UserBalance::getUserId) .collect(Collectors.toSet())); List withdrawList = new ArrayList<>(); Iterator it = balanceList.iterator(); UserBalance ub; while (it.hasNext()) { ub = it.next(); Long userId = ub.getUserId(); log.info("查询提现银行卡userId={}", ub.getUserId()); UserBankCard ubc = userBankCardList.stream().filter(u -> u.getUserId().equals(userId)) .findFirst().orElse(null); if (ubc != null) { withdrawList.add(new UserWithdraw(userId, ubc.getRealName(), ubc.getBankNo(), ub.getBalance())); } else { it.remove(); } } File tmpDir = TempFile.createTempDirectory("export_" + RandomStringUtils.randomAlphabetic(8)); File file1 = new File(tmpDir, DateTimeUtils.format(LocalDate.now(), "yyyyMMdd") + "结算.xlsx"); File file2 = new File(tmpDir, DateTimeUtils.format(LocalDate.now(), "yyyyMMdd") + "结算导入.xls"); EasyExcel.write(file1, UserWithdraw.class) .sheet("sheet").doWrite(withdrawList); InputStream inputStream = getClass().getResourceAsStream("/批量付款到对私银行账户模板.xls"); Workbook workbook = WorkbookFactory.create(inputStream); Sheet sheet = workbook.getSheetAt(0); for (int i = 0; i < withdrawList.size(); i++) { Row row = Optional.ofNullable(sheet.getRow(i + 1)).orElse(sheet.createRow(i + 1)); Optional.ofNullable(row.getCell(1)) .orElse(row.createCell(1)) .setCellValue(withdrawList.get(i).getName()); Optional.ofNullable(row.getCell(2)) .orElse(row.createCell(2)) .setCellValue(withdrawList.get(i).getBankNo()); Optional.ofNullable(row.getCell(3)) .orElse(row.createCell(3)) .setCellValue(withdrawList.get(i).getAmount().doubleValue()); Optional.ofNullable(row.getCell(4)) .orElse(row.createCell(4)) .setCellValue(withdrawList.get(i).getUserId()); } inputStream.close(); FileOutputStream os = new FileOutputStream(file2); workbook.write(os); workbook.close(); File zipFile = ZipUtil.zip(tmpDir); String url = storageService.uploadFromInputStream(new FileInputStream(zipFile), "upload/" + DateTimeUtils.format(LocalDateTime.now(), "yyyyMMddHHmm") + "_" + RandomStringUtils.randomNumeric(8) + ".zip"); ExportWithdraw exportWithdraw = exportWithdrawRepo.save(new ExportWithdraw(url, remark, withdrawList.size(), withdrawList.stream().map(UserWithdraw::getAmount).reduce(BigDecimal::add) .orElse(BigDecimal.ZERO), "处理中")); balanceList.parallelStream().forEach(userBalance -> { log.info("提现userId={}", userBalance.getUserId()); BigDecimal amount = userBalance.getBalance(); userBalance.setLastBalance(userBalance.getBalance()); userBalance.setBalance(BigDecimal.ZERO); userBalanceRepo.saveAndFlush(userBalance); balanceRecordRepo.save(BalanceRecord.builder() .time(LocalDateTime.now()) .userId(userBalance.getUserId()) .amount(amount.negate()) .balance(BigDecimal.ZERO) .lastBalance(userBalance.getLastBalance()) .type(BalanceType.WITHDRAW) .build()); }); tmpDir.delete(); zipFile.delete(); return exportWithdraw; } @Transactional public void importFail(MultipartFile withdrawFile, MultipartFile settleFile) throws IOException { List failSettleList = EasyExcel.read(settleFile.getInputStream()) .head(SandPaySettle.class).sheet() .doReadSync(); failSettleList = failSettleList.stream() .filter(s -> "失败".equals(s.getStatus().trim())) .collect(Collectors.toList()); List withdrawList = EasyExcel.read(withdrawFile.getInputStream()) .head(UserWithdraw.class).sheet() .doReadSync(); List failWithdraw = new ArrayList<>(); for (SandPaySettle sandPaySettle : failSettleList) { List list; if (StringUtils.isNotBlank(sandPaySettle.getRemark()) && Pattern.matches("^\\d+$", sandPaySettle.getRemark() .trim())) { Long userId = Long.parseLong(sandPaySettle.getRemark().trim()); list = withdrawList.stream().filter(i -> i.getUserId().equals(userId)) .collect(Collectors.toList()); } else { list = withdrawList.stream().filter(i -> i.getBankNo().equals(sandPaySettle.getBankNo()) && i.getName().equals(sandPaySettle.getName()) && i.getAmount().compareTo(sandPaySettle.getAmount()) == 0) .collect(Collectors.toList()); } if (list.size() != 1) { throw new BusinessException("不唯一:" + sandPaySettle.getName() + "," + sandPaySettle.getBankNo()); } else { failWithdraw.add(list.get(0)); } } failWithdraw.parallelStream().forEach(withdraw -> { UserBalance userBalance = userBalanceRepo.findById(withdraw.getUserId()) .orElse(new UserBalance(withdraw.getUserId(), BigDecimal.ZERO, BigDecimal.ZERO)); userBalance.setLastBalance(userBalance.getBalance()); userBalance.setBalance(userBalance.getBalance().add(withdraw.getAmount())); userBalanceRepo.saveAndFlush(userBalance); balanceRecordRepo.save(BalanceRecord.builder() .time(LocalDateTime.now()) .userId(userBalance.getUserId()) .amount(withdraw.getAmount()) .balance(userBalance.getBalance()) .lastBalance(userBalance.getLastBalance()) .type(BalanceType.RETURN) .build()); }); } @Async public void autoWithdraw(LocalDate date) { autoWithdrawRecordRepo.findByDate(date).ifPresent(a -> { throw new BusinessException("今日已经提现过"); }); List list = userBalanceRepo.findByBalanceGreaterThanOrderByUserId(BigDecimal.ZERO); AutoWithdrawRecord record = AutoWithdrawRecord.builder() .date(LocalDate.now()) .status("pending") .progress(0) .total(list.size()) .build(); autoWithdrawRecordRepo.saveAndFlush(record); for (UserBalance userBalance : list) { UserBankCard userBankCard = userBankCardRepo.findByUserId(userBalance.getUserId()) .stream().findFirst().orElse(null); if (userBankCard == null) { log.info("自动提现userId={}, amount={}, 未绑卡", userBalance.getBalance(), userBalance.getUserId()); record.setProgress(record.getProgress() + 1); record.setCurrentUserId(userBalance.getUserId()); autoWithdrawRecordRepo.saveAndFlush(record); } else { log.info("自动提现userId={}, amount={}, name={}, bank={}", userBalance.getUserId(), userBalance.getBalance(), userBankCard.getRealName(), userBankCard.getBankNo()); String withdrawId = snowflakeIdWorker.nextId() + ""; BigDecimal amount = userBalance.getBalance(); userBalance.setLastBalance(userBalance.getBalance()); userBalance.setBalance(BigDecimal.ZERO); userBalanceRepo.saveAndFlush(userBalance); balanceRecordRepo.save(BalanceRecord.builder() .time(LocalDateTime.now()) .userId(userBalance.getUserId()) .amount(amount.negate()) .balance(BigDecimal.ZERO) .lastBalance(userBalance.getLastBalance()) .type(BalanceType.WITHDRAW) .withdrawId(withdrawId) .build()); boolean success = false; String msg = null; try { JSONObject res = sandPayService.transfer(withdrawId, userBankCard.getRealName(), userBankCard.getBankNo(), amount); if ("0000".equals(res.getString("respCode"))) { success = true; } else { msg = res.getString("respDesc"); } } catch (Exception e) { msg = e.getMessage(); } if (!success) { userBalance.setLastBalance(userBalance.getBalance()); userBalance.setBalance(userBalance.getBalance().add(amount)); userBalanceRepo.saveAndFlush(userBalance); balanceRecordRepo.save(BalanceRecord.builder() .time(LocalDateTime.now()) .userId(userBalance.getUserId()) .amount(amount) .balance(userBalance.getBalance()) .lastBalance(userBalance.getLastBalance()) .type(BalanceType.RETURN) .withdrawId(withdrawId) .remark(msg) .build()); } record.setProgress(record.getProgress() + 1); record.setCurrentUserId(userBalance.getUserId()); autoWithdrawRecordRepo.saveAndFlush(record); } } record.setStatus("finish"); autoWithdrawRecordRepo.saveAndFlush(record); redisTemplate.delete("autoWithdraw::" + DateTimeUtils.format(date, "yyyyMMdd")); } }