wangqifan 3 سال پیش
والد
کامیت
636d563f49

+ 32 - 0
src/main/java/com/izouma/nineth/domain/AutoWithdrawRecord.java

@@ -0,0 +1,32 @@
+package com.izouma.nineth.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import java.time.LocalDate;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class AutoWithdrawRecord extends BaseEntity {
+
+    @Column(unique = true)
+    private LocalDate date;
+
+    private String remark;
+
+    private long progress;
+
+    private long total;
+
+    private String status;
+
+    private Long currentUserId;
+
+}

+ 48 - 0
src/main/java/com/izouma/nineth/domain/BalanceRecord.java

@@ -0,0 +1,48 @@
+package com.izouma.nineth.domain;
+
+import com.izouma.nineth.enums.BalanceType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Table(indexes = {
+        @Index(columnList = "userId")
+})
+public class BalanceRecord extends BaseEntity {
+
+    private Long userId;
+
+    private LocalDateTime time;
+
+    @Column(precision = 10, scale = 2)
+    private BigDecimal amount;
+
+    @Column(precision = 10, scale = 2)
+    private BigDecimal balance;
+
+    @Column(precision = 10, scale = 2)
+    private BigDecimal lastBalance;
+
+    @Enumerated(EnumType.STRING)
+    @Column(length = 20)
+    private BalanceType type;
+
+    private Long orderId;
+
+    private String withdrawId;
+
+    private String remark;
+
+    private String extra;
+
+}

+ 27 - 0
src/main/java/com/izouma/nineth/domain/ExportWithdraw.java

@@ -0,0 +1,27 @@
+package com.izouma.nineth.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import java.math.BigDecimal;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class ExportWithdraw extends BaseEntity {
+
+    String url;
+
+    String remark;
+
+    Integer total;
+
+    BigDecimal sum;
+
+    String status;
+}

+ 33 - 0
src/main/java/com/izouma/nineth/domain/SettleRecord.java

@@ -0,0 +1,33 @@
+package com.izouma.nineth.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class SettleRecord extends BaseEntity {
+
+    @Column(unique = true)
+    private LocalDate date;
+
+    private long orderNum;
+
+    @Column(precision = 10, scale = 2)
+    private BigDecimal totalAmount;
+
+    @Column(precision = 10, scale = 2)
+    private BigDecimal royaltiesAmount;
+
+    @Column(precision = 10, scale = 2)
+    private BigDecimal serviceChargeAmount;
+}

+ 32 - 0
src/main/java/com/izouma/nineth/domain/UserBalance.java

@@ -0,0 +1,32 @@
+package com.izouma.nineth.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class UserBalance {
+
+    @Id
+    private Long userId;
+
+    private BigDecimal balance;
+
+    private BigDecimal lastBalance;
+
+    private boolean locked = false;
+
+    private String lockReason;
+
+    private LocalDateTime lockTime;
+}

+ 34 - 0
src/main/java/com/izouma/nineth/dto/SandPaySettle.java

@@ -0,0 +1,34 @@
+package com.izouma.nineth.dto;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class SandPaySettle {
+
+    @ExcelProperty("交易流水号")
+    private String id;
+
+    @ExcelProperty("交易金额")
+    private BigDecimal amount;
+
+    @ExcelProperty("状态")
+    private String status;
+
+    @ExcelProperty("收款方名称")
+    private String name;
+
+    @ExcelProperty("收款方账号")
+    private String bankNo;
+
+    @ExcelProperty("交易类型")
+    private String type;
+
+    @ExcelProperty("订单号")
+    private String orderNo;
+
+    @ExcelProperty("附言")
+    private String remark;
+}

+ 26 - 0
src/main/java/com/izouma/nineth/dto/UserWithdraw.java

@@ -0,0 +1,26 @@
+package com.izouma.nineth.dto;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class UserWithdraw {
+
+    @ExcelProperty(value = "用户ID", index = 0)
+    private Long userId;
+
+    @ExcelProperty(value = "姓名", index = 1)
+    private String name;
+
+    @ExcelProperty(value = "银行卡号", index = 2)
+    private String bankNo;
+
+    @ExcelProperty(value = "金额", index = 3)
+    private BigDecimal amount;
+}

+ 18 - 0
src/main/java/com/izouma/nineth/enums/BalanceType.java

@@ -0,0 +1,18 @@
+package com.izouma.nineth.enums;
+
+public enum BalanceType {
+
+    WITHDRAW("提现"),
+    SELL("藏品出售"),
+    RETURN("失败退回");
+
+    private final String description;
+
+    BalanceType(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}

+ 12 - 0
src/main/java/com/izouma/nineth/repo/AutoWithdrawRecordRepo.java

@@ -0,0 +1,12 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.AutoWithdrawRecord;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.time.LocalDate;
+import java.util.Optional;
+
+public interface AutoWithdrawRecordRepo extends JpaRepository<AutoWithdrawRecord, Long> {
+
+    Optional<AutoWithdrawRecord> findByDate(LocalDate date);
+}

+ 14 - 0
src/main/java/com/izouma/nineth/repo/BalanceRecordRepo.java

@@ -0,0 +1,14 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.BalanceRecord;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public interface BalanceRecordRepo extends JpaRepository<BalanceRecord, Long>, JpaSpecificationExecutor<BalanceRecord> {
+    List<BalanceRecord> findByUserIdAndCreatedAtBetweenOrderByCreatedAt(Long userId, LocalDateTime start, LocalDateTime end);
+
+    List<BalanceRecord> findByUserIdOrderByCreatedAt(Long userId);
+}

+ 12 - 0
src/main/java/com/izouma/nineth/repo/ExportWithdrawRepo.java

@@ -0,0 +1,12 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.ExportWithdraw;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public interface ExportWithdrawRepo extends JpaRepository<ExportWithdraw, Long> {
+
+    List<ExportWithdraw> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
+}

+ 4 - 0
src/main/java/com/izouma/nineth/repo/OrderRepo.java

@@ -1,6 +1,7 @@
 package com.izouma.nineth.repo;
 
 import com.izouma.nineth.domain.Order;
+import com.izouma.nineth.enums.CollectionSource;
 import com.izouma.nineth.enums.OrderStatus;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -14,6 +15,9 @@ import java.util.List;
 import java.util.Optional;
 
 public interface OrderRepo extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
+
+    List<Order> findByCreatedAtBetweenAndSourceAndStatusIn(LocalDateTime start, LocalDateTime end, CollectionSource source, Collection<OrderStatus> statuses);
+
     @Query("update Order t set t.del = true where t.id = ?1")
     @Modifying
     @Transactional

+ 13 - 0
src/main/java/com/izouma/nineth/repo/SettleRecordRepo.java

@@ -0,0 +1,13 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.SettleRecord;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.time.LocalDate;
+import java.util.Optional;
+
+public interface SettleRecordRepo extends JpaRepository<SettleRecord, Long> {
+
+    Optional<SettleRecord> findByDate(LocalDate date);
+
+}

+ 23 - 0
src/main/java/com/izouma/nineth/repo/UserBalanceRepo.java

@@ -0,0 +1,23 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.UserBalance;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+import javax.transaction.Transactional;
+import java.math.BigDecimal;
+import java.util.List;
+
+public interface UserBalanceRepo extends JpaRepository<UserBalance, Long>, JpaSpecificationExecutor<UserBalance> {
+
+    List<UserBalance> findByBalanceGreaterThan(BigDecimal balance);
+
+    List<UserBalance> findByLockedFalseAndBalanceGreaterThanOrderByUserId(BigDecimal balance);
+
+    @Query("update UserBalance u set u.locked = false, u.lockTime = null, u.lockReason = null where u.userId = ?1")
+    @Modifying
+    @Transactional
+    int unlock(Long userId);
+}

+ 2 - 0
src/main/java/com/izouma/nineth/repo/UserBankCardRepo.java

@@ -16,4 +16,6 @@ public interface UserBankCardRepo extends JpaRepository<UserBankCard, Long>, Jpa
     @Modifying
     int deleteByUserId(Long userId);
 
+    List<UserBankCard> findByUserIdIn(Iterable<Long> userId);
+
 }

+ 1 - 0
src/main/java/com/izouma/nineth/service/SandPayService.java

@@ -443,6 +443,7 @@ public class SandPayService {
 
 
     public JSONObject transfer(String id, String name, String bank, BigDecimal amount) {
+//        amount = amount.subtract(BigDecimal.valueOf(2));
         JSONObject request = new JSONObject();
         DecimalFormat df = new DecimalFormat("000000000000", DecimalFormatSymbols.getInstance(Locale.US));
         request.put("version", "01");                          //版本号

+ 409 - 0
src/main/java/com/izouma/nineth/service/UserBalanceService.java

@@ -0,0 +1,409 @@
+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.LocalDate;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
+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<String, Object> 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<Order> orders = orderRepo.findByCreatedAtBetweenAndSourceAndStatusIn(date.atStartOfDay(),
+                date.atTime(23, 59, 59, 9999), CollectionSource.TRANSFER,
+                Arrays.asList(OrderStatus.PROCESSING, OrderStatus.FINISH));
+        List<Asset> assets = assetRepo.findAllById(orders.stream().map(Order::getAssetId).collect(Collectors.toSet()));
+
+        BigDecimal totalAmount = BigDecimal.ZERO;
+        BigDecimal royaltiesAmount = BigDecimal.ZERO;
+        BigDecimal serviceChargeAmount = BigDecimal.ZERO;
+
+        List<UserBalance> balanceList = new ArrayList<>();
+        List<BalanceRecord> 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,
+                                false, null, null));
+                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<UserBalance> balanceList = userBalanceRepo.findByBalanceGreaterThan(BigDecimal.ZERO);
+        List<UserBankCard> userBankCardList = balanceList.isEmpty() ? new ArrayList<>() :
+                userBankCardRepo.findByUserIdIn(balanceList.stream().map(UserBalance::getUserId)
+                        .collect(Collectors.toSet()));
+        List<UserWithdraw> withdrawList = new ArrayList<>();
+        Iterator<UserBalance> 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<SandPaySettle> failSettleList = EasyExcel.read(settleFile.getInputStream())
+                .head(SandPaySettle.class).sheet()
+                .doReadSync();
+        failSettleList = failSettleList.stream()
+                .filter(s -> "失败".equals(s.getStatus().trim()))
+                .collect(Collectors.toList());
+        List<UserWithdraw> withdrawList = EasyExcel.read(withdrawFile.getInputStream())
+                .head(UserWithdraw.class).sheet()
+                .doReadSync();
+        List<UserWithdraw> failWithdraw = new ArrayList<>();
+        for (SandPaySettle sandPaySettle : failSettleList) {
+            List<UserWithdraw> 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,
+                            false, null, null));
+            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) throws ExecutionException, InterruptedException {
+        ForkJoinPool customThreadPool = new ForkJoinPool(5);
+        customThreadPool.submit(() -> {
+            autoWithdrawRecordRepo.findByDate(date).ifPresent(a -> {
+                throw new BusinessException("今日已经提现过");
+            });
+
+            List<UserBalance> list = userBalanceRepo
+                    .findByLockedFalseAndBalanceGreaterThanOrderByUserId(BigDecimal.ZERO);
+
+            AutoWithdrawRecord record = AutoWithdrawRecord.builder()
+                    .date(LocalDate.now())
+                    .status("pending")
+                    .progress(0)
+                    .total(list.size())
+                    .build();
+            autoWithdrawRecordRepo.saveAndFlush(record);
+
+            list.parallelStream().forEach(userBalance -> {
+                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);
+
+                    if (!success) {
+                        userBalance.setLocked(true);
+                        userBalance.setLockReason(msg);
+                        userBalance.setLockTime(LocalDateTime.now());
+                        userBalanceRepo.saveAndFlush(userBalance);
+                    }
+                }
+            });
+
+            record.setStatus("finish");
+            autoWithdrawRecordRepo.saveAndFlush(record);
+        }).get();
+    }
+
+    public void revert() throws ExecutionException, InterruptedException {
+        ForkJoinPool customThreadPool = new ForkJoinPool(200);
+        customThreadPool.submit(() -> {
+            LocalDate now = LocalDate.now();
+            userBalanceRepo.findAll().parallelStream().forEach(userBalance -> {
+                List<BalanceRecord> balanceRecords = balanceRecordRepo
+                        .findByUserIdOrderByCreatedAt(userBalance.getUserId());
+                List<BalanceRecord> todayRecords = balanceRecords.stream()
+                        .filter(b -> b.getCreatedAt().toLocalDate().equals(now)).collect(Collectors.toList());
+                List<BalanceRecord> oldRecords = balanceRecords.stream()
+                        .filter(b -> !b.getCreatedAt().toLocalDate().equals(now))
+                        .sorted(Comparator.comparing(BaseEntity::getCreatedAt))
+                        .collect(Collectors.toList());
+                if (oldRecords.size() == 0) {
+                    userBalanceRepo.delete(userBalance);
+                } else {
+                    BalanceRecord record = oldRecords.get(oldRecords.size() - 1);
+                    userBalance.setBalance(record.getBalance());
+                    userBalance.setLastBalance(record.getLastBalance());
+                    userBalanceRepo.save(userBalance);
+                }
+                balanceRecordRepo.deleteAll(todayRecords);
+            });
+        }).get();
+
+    }
+}

+ 81 - 0
src/main/java/com/izouma/nineth/web/UserBalanceController.java

@@ -0,0 +1,81 @@
+package com.izouma.nineth.web;
+
+import com.izouma.nineth.domain.BalanceRecord;
+import com.izouma.nineth.domain.ExportWithdraw;
+import com.izouma.nineth.domain.UserBalance;
+import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.AutoWithdrawRecordRepo;
+import com.izouma.nineth.repo.BalanceRecordRepo;
+import com.izouma.nineth.repo.ExportWithdrawRepo;
+import com.izouma.nineth.repo.UserBalanceRepo;
+import com.izouma.nineth.service.UserBalanceService;
+import com.izouma.nineth.utils.JpaUtils;
+import lombok.AllArgsConstructor;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.util.concurrent.ExecutionException;
+
+@RestController
+@RequestMapping("/userBalance")
+@AllArgsConstructor
+@PreAuthorize("hasRole('ADMIN')")
+public class UserBalanceController extends BaseController {
+
+    private final UserBalanceRepo               userBalanceRepo;
+    private final BalanceRecordRepo             balanceRecordRepo;
+    private final UserBalanceService            userBalanceService;
+    private final ExportWithdrawRepo            exportWithdrawRepo;
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final AutoWithdrawRecordRepo        autoWithdrawRecordRepo;
+
+    @PostMapping("/all")
+    public Page<UserBalance> all(@RequestBody PageQuery pageQuery) {
+        return userBalanceRepo.findAll(JpaUtils.toSpecification(pageQuery, UserBalance.class), JpaUtils.toPageRequest(pageQuery));
+    }
+
+    @PostMapping("/records")
+    public Page<BalanceRecord> records(@RequestBody PageQuery pageQuery) {
+        return balanceRecordRepo.findAll(JpaUtils.toSpecification(pageQuery, BalanceRecord.class), JpaUtils.toPageRequest(pageQuery));
+    }
+
+    @PostMapping("/settle")
+    public void settle(@RequestParam LocalDate start, @RequestParam LocalDate end) {
+        userBalanceService.settle(start, end);
+    }
+
+    @PostMapping(value = "/exportWithdraw")
+    public void exportWithdraw(String remark) throws IOException, InvalidFormatException {
+        userBalanceService.exportWithdrawAsync(remark);
+    }
+
+    @PostMapping(value = "/exportHistory")
+    public Page<ExportWithdraw> exportHistory(Pageable pageable) {
+        return exportWithdrawRepo.findAll(pageable);
+    }
+
+    @PostMapping("/importFail")
+    public void importFail(@RequestPart("withdrawList") MultipartFile withdrawFile,
+                           @RequestPart("failList") MultipartFile failFile) throws IOException {
+        userBalanceService.importFail(withdrawFile, failFile);
+    }
+
+    @PostMapping("/autoWithdraw")
+    public void autoWithdraw() throws ExecutionException, InterruptedException {
+
+        LocalDate date = LocalDate.now();
+        autoWithdrawRecordRepo.findByDate(date).ifPresent(a -> {
+            throw new BusinessException("今日已经提现过");
+        });
+
+        userBalanceService.autoWithdraw(date);
+    }
+}

+ 436 - 0
src/main/vue/src/views/UserBalance.vue

@@ -0,0 +1,436 @@
+<template>
+    <div class="list-view">
+        <page-title> </page-title>
+        <div class="filters-container">
+            <el-input
+                placeholder="搜索..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+            <template v-if="$store.state.userInfo && $store.state.userInfo.username === 'xiong'">
+                <el-popover class="filter-item" placement="bottom" width="350" trigger="click" v-model="showSettlePop">
+                    <el-date-picker
+                        type="daterange"
+                        value-format="yyyy-MM-dd"
+                        v-model="date"
+                        start-placeholder="开始日期"
+                        end-placeholder="结束日期"
+                        :disabled="settleing"
+                    ></el-date-picker>
+                    <el-button
+                        style="float: right; margin-top: 10px"
+                        size="mini"
+                        type="primary"
+                        @click="settle"
+                        :loading="settleing"
+                    >
+                        确认
+                    </el-button>
+                    <el-button @click="showSettlePop = false" style="float: right; margin: 10px 10px 0 0" size="mini">
+                        取消
+                    </el-button>
+                    <el-button slot="reference" size="mini" type="primary">结算</el-button>
+                </el-popover>
+                <el-button
+                    class="filter-item"
+                    type="primary"
+                    @click="exportWithdraw"
+                    :loading="downloading"
+                    size="mini"
+                >
+                    全部提现并导出Excel
+                </el-button>
+                <el-button
+                    class="filter-item"
+                    type="primary"
+                    @click="autoWithdraw"
+                    :loading="autoWithdrawing"
+                    size="mini"
+                >
+                    自动提现
+                </el-button>
+                <el-button
+                    class="filter-item"
+                    type="primary"
+                    @click="(showHisotryDialog = true), (history.page = 1), getHistory()"
+                    size="mini"
+                >
+                    历史记录
+                </el-button>
+                <el-button class="filter-item" type="primary" @click="showImportDialog = true" size="mini">
+                    导入结果
+                </el-button>
+            </template>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column type="index" label="#"></el-table-column>
+            <el-table-column prop="userId" label="用户ID"></el-table-column>
+            <el-table-column prop="balance" label="余额"></el-table-column>
+            <el-table-column prop="locked" label="锁定" width="100" align="center">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+                <template v-slot="{ row }">
+                    <el-tag :type="row.locked ? 'danger' : 'info'">{{ row.locked ? '是' : '否' }}</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column prop="lockTime" label="锁定时间" width="150"></el-table-column>
+            <el-table-column prop="lockReason" label="锁定原因" show-overflow-tooltip></el-table-column>
+            <el-table-column width="100" label="操作" align="center">
+                <template v-slot="{ row }">
+                    <el-button size="mini" type="primary" plain @click="showRecords(row.userId)">查看记录</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+
+        <el-dialog :visible.sync="showSettleDialog" title="选择日期" width="500">
+            <el-date-picker
+                type="daterange"
+                value-format="yyyy-MM-dd"
+                v-model="date"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+            ></el-date-picker>
+            <div slot="footer">
+                <el-button size="mini" @click="showSettleDialog = false" :disabled="settleing">取消</el-button>
+                <el-button size="mini" @click="settle" :loading="settleing" type="primary">确认</el-button>
+            </div>
+        </el-dialog>
+
+        <el-dialog :visible.sync="showImportDialog" title="导入结果">
+            <el-form>
+                <el-form-item label="提现导出文件">
+                    <input @change="change('withdrawFile', $event)" type="file" />
+                </el-form-item>
+                <el-form-item label="提现结果文件">
+                    <input @change="change('settleFile', $event)" type="file" />
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="showImportDialog = false" size="mini" :disabled="importFailLoading">取消</el-button>
+                <el-button @click="importFail" size="mini" type="primary" :loading="importFailLoading">导入</el-button>
+            </div>
+        </el-dialog>
+
+        <el-dialog :visible.sync="showRecordDialog" title="余额记录" width="1000px">
+            <el-table :data="record.list" height="calc(100vh - 500px)" v-loading="record.loading">
+                <el-table-column prop="createdAt" label="时间" width="150"></el-table-column>
+                <el-table-column prop="amount" label="变更金额" width="85">
+                    <template v-slot="{ row }">
+                        <span :style="{ color: row.amount >= 0 ? '#f56c6c' : '#67c23a' }">
+                            {{ row.amount >= 0 ? '+' : '' }}{{ row.amount }}
+                        </span>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="balance" label="余额" width="85"></el-table-column>
+                <el-table-column prop="lastBalance" label="上次余额" width="85"></el-table-column>
+                <el-table-column
+                    prop="type"
+                    label="类型"
+                    width="80"
+                    :formatter="balanceTypeFormatter"
+                ></el-table-column>
+                <el-table-column prop="withdrawId" label="提现订单ID" show-overflow-tooltip></el-table-column>
+                <el-table-column prop="remark" label="备注" show-overflow-tooltip></el-table-column>
+            </el-table>
+            <el-pagination
+                style="margin-top: 10px"
+                background
+                @size-change="onTableSizeChange($event, 'record')"
+                @current-change="onTableCurrentChange($event, 'record')"
+                :current-page="record.page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="record.pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="record.totalElements"
+            ></el-pagination>
+        </el-dialog>
+
+        <el-dialog :visible.sync="showHisotryDialog" title="历史记录" width="600px">
+            <el-table :data="history.list" height="calc(100vh - 500px)" v-loading="history.loading">
+                <el-table-column prop="createdAt" label="时间" width="150"></el-table-column>
+                <el-table-column prop="total" label="总数"></el-table-column>
+                <el-table-column prop="sum" label="总金额"></el-table-column>
+                <el-table-column prop="lastBalance" label="操作" width="80">
+                    <template v-slot="{ row }">
+                        <el-button @click="download(row)" type="text">下载</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <el-pagination
+                style="margin-top: 10px"
+                background
+                @size-change="onTableSizeChange($event, 'history')"
+                @current-change="onTableCurrentChange($event, 'history')"
+                :current-page="record.page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="history.pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="history.totalElements"
+            ></el-pagination>
+        </el-dialog>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+import { getDay, addDays, format } from 'date-fns';
+export default {
+    name: 'UserBalance',
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/userBalance/all',
+            downloading: false,
+            typeOptions: [
+                { label: '默认', value: 'DEFAULT' },
+                { label: '盲盒', value: 'BLIND_BOX' },
+                { label: '拍卖', value: 'AUCTION' },
+                { label: '展厅', value: 'SHOWROOM' }
+            ],
+            sourceOptions: [
+                { label: '官方', value: 'OFFICIAL' },
+                { label: '用户铸造', value: 'USER' },
+                { label: '转让', value: 'TRANSFER' }
+            ],
+            balanceTypeOptions: [
+                { label: '提现', value: 'WITHDRAW' },
+                { label: '藏品出售', value: 'SELL' },
+                { label: '失败退回', value: 'RETURN' }
+            ],
+            sortStr: 'balance,desc',
+            date: null,
+            showSettleDialog: false,
+            showSettlePop: false,
+            settleing: false,
+            showImportDialog: false,
+            withdrawFile: null,
+            settleFile: null,
+            importFailLoading: false,
+            showRecordDialog: false,
+            record: {
+                list: [],
+                userId: null,
+                page: 1,
+                pageSize: 20,
+                totalElements: 0,
+                loading: false
+            },
+            showHisotryDialog: false,
+            history: {
+                list: [],
+                userId: null,
+                page: 1,
+                pageSize: 20,
+                totalElements: 0,
+                loading: false
+            },
+            autoWithdrawing: false
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    created() {
+        let week = getDay(new Date());
+        if (week === 1) {
+            this.date = [format(addDays(new Date(), -3), 'yyyy-MM-dd'), format(addDays(new Date(), -1), 'yyyy-MM-dd')];
+        } else {
+            this.date = [format(addDays(new Date(), -1), 'yyyy-MM-dd'), format(addDays(new Date(), -1), 'yyyy-MM-dd')];
+        }
+    },
+    methods: {
+        balanceTypeFormatter(row, column, cellValue, index) {
+            return (this.balanceTypeOptions.find(i => i.value === cellValue) || {}).label;
+        },
+        beforeGetData() {
+            return {
+                query: {
+                    userId: this.search
+                }
+            };
+        },
+        settle() {
+            if (!(this.date && this.date.length === 2)) {
+                this.$message.error('请选择日期');
+                return;
+            }
+            this.settleing = true;
+            this.$http
+                .post('/userBalance/settle', { start: this.date[0], end: this.date[1] })
+                .then(res => {
+                    this.settleing = false;
+                    this.showSettleDialog = false;
+                    this.showSettlePop = false;
+                    this.$message.success('成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    this.settleing = false;
+                    this.showSettleDialog = false;
+                    this.showSettlePop = false;
+                    this.$message.error(e.error || '失败');
+                });
+        },
+        exportWithdraw() {
+            this.downloading = true;
+            this.$http
+                .post('/userBalance/exportWithdraw')
+                .then(res => {
+                    this.downloading = false;
+                    //window.open(res.url, '_blank');
+                    this.getData();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        change(file, e) {
+            if (e.target.files && e.target.files[0]) {
+                this[file] = e.target.files[0];
+            }
+        },
+        importFail() {
+            if (!this.settleFile) {
+                this.$message.error('请选择提现结果文件');
+                return;
+            }
+            if (!this.withdrawFile) {
+                this.$message.error('请选择提现导出文件');
+                return;
+            }
+            this.importFailLoading = true;
+            let form = new FormData();
+            form.append('withdrawList', this.withdrawFile);
+            form.append('failList', this.settleFile);
+            this.$axios
+                .post('/userBalance/importFail', form)
+                .then(res => {
+                    this.importFailLoading = false;
+                    this.showImportDialog = false;
+                    this.$message.success('成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    this.importFailLoading = false;
+                    this.$message.error(e.error || '失败');
+                });
+        },
+        showRecords(userId) {
+            this.userId = userId;
+            this.record.list = [];
+            this.showRecordDialog = true;
+            this.record.page = 1;
+            this.getRecords();
+        },
+        getRecords() {
+            this.record.loading = true;
+            this.$http
+                .post(
+                    '/userBalance/records',
+                    {
+                        query: { userId: this.userId },
+                        page: this.record.page - 1,
+                        size: this.record.pageSize,
+                        sort: 'id,desc'
+                    },
+                    { body: 'json' }
+                )
+                .then(res => {
+                    this.record.loading = false;
+                    this.record.list = res.content;
+                    this.record.totalElements = Number(res.totalElements);
+                })
+                .catch(e => {
+                    this.record.loading = false;
+                });
+        },
+        onTableSizeChange(e, type) {
+            this[type].page = 1;
+            this[type].pageSize = e;
+            if (type === 'record') {
+                this.getRecords();
+            } else if (type === 'history') {
+                this.getHistory();
+            }
+        },
+        onTableCurrentChange(e, type) {
+            console.log(e, type);
+            this[type].page = e;
+            if (type === 'record') {
+                this.getRecords();
+            } else if (type === 'history') {
+                this.getHistory();
+            }
+        },
+        getHistory() {
+            this.history.loading = true;
+            this.$http
+                .post('/userBalance/exportHistory', {
+                    page: this.history.page - 1,
+                    size: this.history.pageSize,
+                    sort: 'createdAt,desc'
+                })
+                .then(res => {
+                    this.history.loading = false;
+                    this.history.list = res.content;
+                    this.history.totalElements = Number(res.totalElements);
+                })
+                .catch(e => {
+                    this.history.loading = false;
+                });
+        },
+        download(row) {
+            window.open(row.url, '_blank');
+        },
+        autoWithdraw() {
+            this.autoWithdrawing = true;
+            this.$http
+                .post('/userBalance/autoWithdraw')
+                .then(res => {
+                    this.$message.success('成功');
+                    this.autoWithdrawing = false;
+                })
+                .catch(e => {
+                    this.$message.error(e.error || '失败');
+                    this.autoWithdrawing = false;
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 13 - 0
src/test/java/com/izouma/nineth/service/UserServiceTest.java

@@ -198,4 +198,17 @@ public class UserServiceTest extends ApplicationTests {
 
         identityAuthService.auth(identityAuthRepo.findById(590L).orElseThrow(new BusinessException("2")));
     }
+
+    @Test
+    public void setSettleAcountId() {
+        List<UserBankCard> userBankCards = userBankCardRepo.findAll();
+        userBankCards.forEach(userBankCard -> {
+            User user = userRepo.findById(userBankCard.getUserId()).orElse(User.builder().id(100L).build());
+            if (user.getId() != 100L) {
+                user.setSettleAccountId("1");
+                user.setMemberId(user.getId().toString());
+                userRepo.save(user);
+            }
+        });
+    }
 }