xiongzhu %!s(int64=3) %!d(string=hai) anos
pai
achega
8cb481d003

+ 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;
+
+}

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

@@ -0,0 +1,22 @@
+package com.izouma.nineth.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class ExportWithdraw extends BaseEntity {
+
+    String url;
+
+    String remark;
+
+
+}

+ 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;
+}

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

@@ -0,0 +1,24 @@
+package com.izouma.nineth.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import java.math.BigDecimal;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+public class UserBalance {
+
+    @Id
+    private Long userId;
+
+    private BigDecimal balance;
+
+    private BigDecimal lastBalance;
+
+}

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

@@ -0,0 +1,31 @@
+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;
+}

+ 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;
+    }
+}

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

@@ -0,0 +1,8 @@
+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;
+
+public interface BalanceRecordRepo extends JpaRepository<BalanceRecord, Long>, JpaSpecificationExecutor<BalanceRecord> {
+}

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

@@ -0,0 +1,13 @@
+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;
+import java.util.Optional;
+
+public interface ExportWithdrawRepo extends JpaRepository<ExportWithdraw, Long> {
+
+    List<ExportWithdraw> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
+}

+ 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);
+
+}

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

@@ -0,0 +1,15 @@
+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 java.math.BigDecimal;
+import java.util.List;
+
+public interface UserBalanceRepo extends JpaRepository<UserBalance, Long>, JpaSpecificationExecutor<UserBalance> {
+
+    List<UserBalance> findByBalanceGreaterThan(BigDecimal balance);
+
+
+}

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

@@ -0,0 +1,225 @@
+package com.izouma.nineth.service;
+
+import cn.hutool.core.util.ZipUtil;
+import com.alibaba.excel.EasyExcel;
+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 lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.RandomStringUtils;
+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.stereotype.Service;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+@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;
+
+    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));
+
+        BigDecimal totalAmount = BigDecimal.ZERO;
+        BigDecimal royaltiesAmount = BigDecimal.ZERO;
+        BigDecimal serviceChargeAmount = BigDecimal.ZERO;
+
+        for (Order order : orders) {
+            Asset asset = assetRepo.findById(order.getAssetId()).orElseThrow(new BusinessException("藏品不存在"));
+            UserBalance userBalance = userBalanceRepo.findById(asset.getUserId()).orElse(null);
+            if (userBalance == null) {
+                userBalance = userBalanceRepo.saveAndFlush(new UserBalance(asset.getUserId(), BigDecimal.ZERO, BigDecimal.ZERO));
+            }
+
+            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 = order.getTotalPrice()
+                    .subtract(order.getGasPrice())
+                    .multiply(BigDecimal.valueOf(order.getRoyalties()))
+                    .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
+            serviceChargeAmount = 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));
+            userBalanceRepo.saveAndFlush(userBalance);
+
+            balanceRecordRepo.save(BalanceRecord.builder()
+                    .time(LocalDateTime.now())
+                    .userId(asset.getUserId())
+                    .orderId(order.getId())
+                    .amount(amount)
+                    .balance(userBalance.getBalance())
+                    .lastBalance(userBalance.getLastBalance())
+                    .type(BalanceType.SELL)
+                    .build());
+        }
+        settleRecordRepo.save(new SettleRecord(date, orders.size(), totalAmount, royaltiesAmount, serviceChargeAmount));
+    }
+
+    @Transactional
+    public ExportWithdraw exportWithdraw(String remark) throws IOException, InvalidFormatException {
+        List<UserBalance> balanceList = userBalanceRepo.findByBalanceGreaterThan(BigDecimal.ZERO);
+        List<UserWithdraw> withdrawList = new ArrayList<>();
+        balanceList.forEach(userBalance -> {
+            userBankCardRepo.findByUserId(userBalance.getUserId()).stream().findFirst()
+                    .ifPresent(userBankCard -> withdrawList.add(new UserWithdraw(userBalance.getUserId(), userBankCard.getRealName(),
+                            userBankCard.getBankNo(), userBalance.getBalance())));
+        });
+
+        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());
+        }
+
+        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));
+        for (UserBalance userBalance : balanceList) {
+            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) {
+            withdrawList.stream().filter(i -> i.getBankNo().equals(sandPaySettle.getBankNo()))
+                    .findAny().ifPresent(failWithdraw::add);
+        }
+        for (UserWithdraw withdraw : failWithdraw) {
+            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());
+        }
+    }
+}

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

@@ -0,0 +1,70 @@
+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.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.commons.io.IOUtils;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.time.LocalDate;
+
+@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;
+
+    @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 ExportWithdraw exportWithdraw(String remark) throws IOException, InvalidFormatException {
+        return userBalanceService.exportWithdraw(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);
+    }
+}

BIN=BIN
src/main/resources/批量付款到对私银行账户模板.xls


+ 8 - 0
src/main/vue/src/router.js

@@ -554,6 +554,14 @@ const router = new Router({
                     meta: {
                         title: '结算查询'
                     }
+                },
+                {
+                    path: '/userBalance',
+                    name: 'UserBalance',
+                    component: () => import(/* webpackChunkName: "userBalance" */ '@/views/UserBalance.vue'),
+                    meta: {
+                        title: '用户余额'
+                    }
                 }
                 /**INSERT_LOCATION**/
             ]

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

@@ -0,0 +1,383 @@
+<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>
+            <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="(showHisotryDialog = true), getHistory()" size="mini">
+                历史记录
+            </el-button>
+            <el-button class="filter-item" type="primary" @click="showImportDialog = true" size="mini">
+                导入结果
+            </el-button>
+        </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 prop="userId" label="用户ID"></el-table-column>
+            <el-table-column prop="balance" label="余额"></el-table-column>
+            <el-table-column width="100">
+                <template v-slot="{ row }">
+                    <el-button size="mini" type="primary" @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="600">
+            <el-table :data="record.list" height="calc(100vh - 500px)" v-loading="record.loading">
+                <el-table-column prop="createdAt" label="时间"></el-table-column>
+                <el-table-column prop="amount" label="金额">
+                    <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="余额"></el-table-column>
+                <el-table-column prop="lastBalance" label="上次余额"></el-table-column>
+                <el-table-column prop="type" label="类型" :formatter="balanceTypeFormatter"></el-table-column>
+            </el-table>
+            <el-pagination
+                style="margin-top: 10px"
+                background
+                @size-change="onSizeChange($event, 'record')"
+                @current-change="onCurrentChange($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="500">
+            <el-table :data="history.list" height="calc(100vh - 500px)" v-loading="history.loading">
+                <el-table-column prop="createdAt" label="时间"></el-table-column>
+                <el-table-column prop="lastBalance" label="操作" width="100">
+                    <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="onSizeChange($event, 'history')"
+                @current-change="onCurrentChange($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
+            }
+        };
+    },
+    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.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;
+                });
+        },
+        onSizeChange(e, type) {
+            this[type].page = 1;
+            this[type].pageSize = e;
+            if (type === 'record') {
+                this.getRecords();
+            } else if (type === 'history') {
+                this.getHistory();
+            }
+        },
+        onCurrentChange(e) {
+            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: 'id,desc'
+                    },
+                    { body: 'json' }
+                )
+                .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');
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 61 - 2
src/test/java/com/izouma/nineth/CommonTest.java

@@ -1,5 +1,6 @@
 package com.izouma.nineth;
 
+import com.alibaba.excel.EasyExcel;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.serializer.SerializerFeature;
 import com.github.kevinsawicki.http.HttpRequest;
@@ -7,8 +8,11 @@ import com.google.common.base.Splitter;
 import com.izouma.nineth.config.Constants;
 import com.izouma.nineth.domain.BaseEntity;
 import com.izouma.nineth.domain.BlindBoxItem;
+import com.izouma.nineth.domain.SettleRecord;
 import com.izouma.nineth.domain.User;
 import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.dto.SandPaySettle;
+import com.izouma.nineth.dto.UserWithdraw;
 import com.izouma.nineth.utils.AESEncryptUtil;
 import com.izouma.nineth.utils.TokenUtils;
 import com.izouma.nineth.web.BaseController;
@@ -30,6 +34,8 @@ import org.apache.commons.lang3.Range;
 import org.apache.commons.text.CaseUtils;
 import org.apache.http.NameValuePair;
 import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
+import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.util.TempFile;
 import org.bouncycastle.util.encoders.Base64;
 import org.bytedeco.javacv.FFmpegFrameGrabber;
@@ -53,11 +59,11 @@ import org.springframework.web.bind.annotation.RestController;
 
 import javax.imageio.ImageIO;
 import java.awt.*;
+import java.awt.Font;
 import java.awt.font.FontRenderContext;
 import java.awt.geom.AffineTransform;
 import java.awt.image.BufferedImage;
-import java.io.File;
-import java.io.IOException;
+import java.io.*;
 import java.lang.reflect.Method;
 import java.math.BigDecimal;
 import java.math.BigInteger;
@@ -564,4 +570,57 @@ public class CommonTest {
         DecimalFormat df = new DecimalFormat("000000", DecimalFormatSymbols.getInstance(Locale.US));
         System.out.println(df.format(new BigDecimal("199.11")));       // prints: 001.0
     }
+
+    @Test
+    public void testWriteExcel() throws IOException, InvalidFormatException {
+        // Creating file object of existing excel file
+        File xlsxFile = new File("/Users/drew/Downloads/批量付款到对私银行账户模板的副本.xls");
+
+        InputStream inputStream = getClass().getResourceAsStream("/批量付款到对私银行账户模板.xls");
+
+        //Creating workbook from input stream
+        Workbook workbook = WorkbookFactory.create(inputStream);
+
+        //Reading first sheet of excel file
+        Sheet sheet = workbook.getSheetAt(0);
+
+
+        for (int i = 0; i < 100; i++) {
+            Row row = sheet.getRow(i + 1);
+            if (row == null) {
+                row = sheet.createRow(i + 1);
+            }
+            Optional.ofNullable(row.getCell(1)).orElse(row.createCell(1)).setCellValue("1");
+            Optional.ofNullable(row.getCell(2)).orElse(row.createCell(2)).setCellValue("2");
+            Optional.ofNullable(row.getCell(3)).orElse(row.createCell(3))
+                    .setCellValue(new BigDecimal("1").doubleValue());
+        }
+
+        //Close input stream
+        inputStream.close();
+
+        FileOutputStream os = new FileOutputStream(xlsxFile);
+        workbook.write(os);
+
+        //Close the workbook and output stream
+        workbook.close();
+    }
+
+    @Test
+    public void testSandImport() {
+        List<SandPaySettle> failSettleList = EasyExcel.read("/Users/drew/Downloads/0411失败.csv")
+                .head(SandPaySettle.class).sheet()
+                .doReadSync();
+        List<UserWithdraw> withdrawList = EasyExcel.read("/Users/drew/Downloads/0411.xlsx")
+                .head(UserWithdraw.class).sheet()
+                .doReadSync();
+        List<UserWithdraw> failWithdraw = new ArrayList<>();
+        for (SandPaySettle sandPaySettle : failSettleList) {
+            withdrawList.stream().filter(i -> i.getBankNo().equals(sandPaySettle.getBankNo()))
+                    .findAny().ifPresent(failWithdraw::add);
+        }
+
+        System.out.println(failSettleList.size());
+        System.out.println(failWithdraw.size());
+    }
 }

+ 25 - 0
src/test/java/com/izouma/nineth/service/UserBalanceServiceTest.java

@@ -0,0 +1,25 @@
+package com.izouma.nineth.service;
+
+import com.izouma.nineth.ApplicationTests;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class UserBalanceServiceTest extends ApplicationTests {
+
+    @Autowired
+    private UserBalanceService userBalanceService;
+
+    @Test
+    void settle() {
+        userBalanceService.settle(LocalDate.of(2022, 04, 8), LocalDate.of(2022, 4, 10));
+
+    }
+
+    @Test
+    void settleFile() {
+    }
+}