Kaynağa Gözat

Merge branch 'master_dev_0615' into master_dev

# Conflicts:
#	crm-manager/src/main/resources/application-dev.yml
kongxiangyang 4 saat önce
ebeveyn
işleme
6c5e7d4bdc

+ 6 - 2
crm-manager/src/main/java/com/crm/manager/controller/VaultodyController.java

@@ -1,5 +1,6 @@
 package com.crm.manager.controller;
 
+import com.crm.manager.handler.TotalRowWriteHandler;
 import com.crm.manager.service.VaultodyService;
 import com.crm.manager.util.BlockchainUtils;
 import com.crm.manager.util.DateUtils;
@@ -73,8 +74,11 @@ public class VaultodyController {
             exportDto.setCreatedTimestamp(DateUtils.secondToDateTimeStr(item.getCreatedTimestamp()));
             exportDtos.add(exportDto);
         }
-        ExportUtil.transferToResponse(FileProcessUtil.genExportFileName("VAULTODY_VAULT_TRANSACTIONS"), exportDtos,
-                TransactionItemExport.class, response);
+
+        String fileName = FileProcessUtil.genExportFileName("VAULTODY_VAULT_TRANSACTIONS");
+        // 改动:传入完整数据列表,不再只传size
+        TotalRowWriteHandler totalHandler = new TotalRowWriteHandler(exportDtos);
+        ExportUtil.exportWithCallback(fileName, exportDtos, TransactionItemExport.class, totalHandler);
     }
 
 }

+ 2 - 0
crm-manager/src/main/java/com/crm/manager/dao/mapper/TransactionItemMapper.java

@@ -12,4 +12,6 @@ public interface TransactionItemMapper {
     Integer countList(@Param("entity") VaultTransactionsSearchEntity entity, @Param("startSecond") Long startSecond, @Param("endSecond") Long endSecond);
 
     List<TransactionItemTable> pageList(@Param("entity") VaultTransactionsSearchEntity entity, @Param("startSecond") Long startSecond, @Param("endSecond") Long endSecond);
+
+    TransactionItemTable getBeforeYesterdayLast(String vaultId);
 }

+ 110 - 0
crm-manager/src/main/java/com/crm/manager/handler/TotalRowWriteHandler.java

@@ -0,0 +1,110 @@
+package com.crm.manager.handler;
+
+import com.alibaba.excel.write.handler.WorkbookWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+import com.crm.rely.backend.model.dto.export.TransactionItemExport;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+
+public class TotalRowWriteHandler implements WorkbookWriteHandler {
+
+    // 数据集合 + 数据条数(新增:接收原始数据用于Java求和)
+    private final List<TransactionItemExport> dataList;
+    private final int totalDataCount;
+
+    public TotalRowWriteHandler(List<TransactionItemExport> dataList) {
+        this.dataList = dataList;
+        this.totalDataCount = dataList.size();
+    }
+
+    @Override
+    public void afterWorkbookDispose(WriteWorkbookHolder writeWorkbookHolder) {
+        Workbook workbook = writeWorkbookHolder.getWorkbook();
+        Sheet sheet = workbook.getSheetAt(0);
+        if (sheet == null || totalDataCount <= 0) {
+            return;
+        }
+
+        // ========== 1. Java 后台手动计算三项合计 ==========
+        BigDecimal sumSenderAmount = BigDecimal.ZERO;
+        BigDecimal sumRecipientAmount = BigDecimal.ZERO;
+        BigDecimal sumFeeAmount = BigDecimal.ZERO;
+
+        for (TransactionItemExport item : dataList) {
+            // 转出金额累加
+            sumSenderAmount = addAmount(sumSenderAmount, item.getSenderAmount());
+            // 收到金额累加
+            sumRecipientAmount = addAmount(sumRecipientAmount, item.getRecipientAmount());
+            // 手续费累加
+            sumFeeAmount = addAmount(sumFeeAmount, item.getFeeAmount());
+        }
+
+        // 保留4位小数,和业务数据格式对齐
+        sumSenderAmount = sumSenderAmount.setScale(4, RoundingMode.HALF_UP);
+        sumRecipientAmount = sumRecipientAmount.setScale(4, RoundingMode.HALF_UP);
+        sumFeeAmount = sumFeeAmount.setScale(4, RoundingMode.HALF_UP);
+
+        // ========== 2. 创建合计行 & 样式 ==========
+        int totalRowIndex = totalDataCount + 1;
+        Row totalRow = sheet.createRow(totalRowIndex);
+
+        // A-E列样式:仅加粗,无灰色背景
+        CellStyle textCellStyle = workbook.createCellStyle();
+        Font boldFont = workbook.createFont();
+        boldFont.setBold(true);
+        textCellStyle.setFont(boldFont);
+
+        // 金额列样式:加粗 + 灰色背景
+        CellStyle amountCellStyle = workbook.createCellStyle();
+        Font amountFont = workbook.createFont();
+        amountFont.setBold(true);
+        amountCellStyle.setFont(amountFont);
+        amountCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+        amountCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+
+        // ========== 3. 填充单元格 ==========
+        // 合并A-E列,写入合计
+        Cell titleCell = totalRow.createCell(0);
+        titleCell.setCellValue("合计");
+        titleCell.setCellStyle(textCellStyle);
+        sheet.addMergedRegion(new CellRangeAddress(totalRowIndex, totalRowIndex, 0, 4));
+
+        // 转出金额 F列(5)
+        Cell sendCell = totalRow.createCell(5);
+        sendCell.setCellValue(sumSenderAmount.doubleValue());
+        sendCell.setCellStyle(amountCellStyle);
+
+        // 收到金额 J列(9)
+        Cell recCell = totalRow.createCell(9);
+        recCell.setCellValue(sumRecipientAmount.doubleValue());
+        recCell.setCellStyle(amountCellStyle);
+
+        // 区块链手续费 L列(11)
+        Cell feeCell = totalRow.createCell(11);
+        feeCell.setCellValue(sumFeeAmount.doubleValue());
+        feeCell.setCellStyle(amountCellStyle);
+    }
+
+    /**
+     * 字符串金额转BigDecimal并累加,兼容空值
+     */
+    private BigDecimal addAmount(BigDecimal total, String amountStr) {
+        if (amountStr == null || amountStr.trim().isEmpty()) {
+            return total;
+        }
+        try {
+            // 只截取纯数字部分(如果字段带币种/后缀,可根据实际格式微调)
+            String numStr = amountStr.trim().replaceAll("[^0-9.]", "");
+            if (numStr.isEmpty()) {
+                return total;
+            }
+            return total.add(new BigDecimal(numStr));
+        } catch (Exception e) {
+            return total;
+        }
+    }
+}

+ 163 - 12
crm-manager/src/main/java/com/crm/manager/service/impl/VaultodyServiceImpl.java

@@ -1,5 +1,7 @@
 package com.crm.manager.service.impl;
 
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
 import com.alibaba.fastjson2.JSON;
 import com.crm.manager.dao.mapper.TransactionItemMapper;
 import com.crm.manager.repository.TransactionItemRepository;
@@ -16,6 +18,8 @@ import com.crm.rely.backend.core.pojo.table.SysConfigTable;
 import com.crm.rely.backend.model.config.VaultodyConfig;
 import com.crm.rely.backend.model.config.VaultodyOrderConfig;
 import com.crm.rely.backend.model.constant.ConfigConstants;
+import com.crm.rely.backend.model.dto.vaultody.TransactionItemResultDto;
+import com.crm.rely.backend.model.dto.vaultody.TransactionItemSearchDto;
 import com.crm.rely.backend.model.dto.vaultody.vaults.TransactionItemDto;
 import com.crm.rely.backend.model.dto.vaultody.vaults.VaultTransaction;
 import com.crm.rely.backend.model.dto.vaultody.vaults.VaultodyVaultsListDto;
@@ -46,6 +50,7 @@ import org.springframework.util.ObjectUtils;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
 import java.io.IOException;
+import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
 import java.util.*;
 import java.util.stream.Collectors;
@@ -461,7 +466,7 @@ public class VaultodyServiceImpl implements VaultodyService {
     public BaseResultDto searchList(VaultTransactionsSearchEntity entity) throws Exception {
         List<TransactionItemTable> tables = new LinkedList<>();
         VaultodyConfig vaultodyConfig = getVaultodyConfig(entity.getVaultId());
-        List<TransactionItemDto> list = queryWithFilter(vaultodyConfig);
+        List<TransactionItemDto> list = queryWithFilterHalfDay(vaultodyConfig);
         if(!CollectionUtils.isEmpty(list)){
             for (TransactionItemDto transactionItemDto : list) {
                 TransactionItemTable table = new TransactionItemTable() ;
@@ -502,26 +507,158 @@ public class VaultodyServiceImpl implements VaultodyService {
             throw new ServiceException(Constants.SYSTEM_ERROR);
         }
 
-        return ResultWithPagerDto.success(pageDto, dtos);
+        List<TransactionItemSearchDto> r = new ArrayList<>(dtos.size());
+        // 转出金额
+        BigDecimal totalSenderAmount = new BigDecimal(0);
+        // 收到金额
+        BigDecimal totalRecipientAmount = new BigDecimal(0);
+        // 手续费
+        BigDecimal totalFeeAmount = new BigDecimal(0);
+        for (TransactionItemTable table : dtos) {
+            TransactionItemSearchDto dto = new TransactionItemSearchDto();
+            BeanUtils.copyProperties(table, dto);
+            totalSenderAmount = totalSenderAmount.add(new BigDecimal(table.getSenderAmount()));
+            totalRecipientAmount = totalRecipientAmount.add(new BigDecimal(table.getRecipientAmount()));
+            totalFeeAmount = totalFeeAmount.add(new BigDecimal(table.getFeeAmount()));
+            r.add(dto);
+        }
+        TransactionItemResultDto resultDto = new TransactionItemResultDto();
+        resultDto.setList(r);
+        resultDto.setTotalSenderAmount(totalSenderAmount);
+        resultDto.setTotalRecipientAmount(totalRecipientAmount);
+        resultDto.setTotalFeeAmount(totalFeeAmount);
+        resultDto.setPage(pageDto);
+        return BaseResultDto.success(resultDto);
     }
 
     public List<TransactionItemDto> queryWithFilter(VaultodyConfig config) throws Exception {
         VaultTransactionsEntity entity = new VaultTransactionsEntity();
         entity.setVaultId(config.getVaultId());
+
+        // 1、查询前天最后一条,用它的itemId做startingAfter
+        TransactionItemTable lastBeforeYes = transactionItemMapper.getBeforeYesterdayLast(config.getVaultId());
+        if (lastBeforeYes != null && StringUtils.isNotBlank(lastBeforeYes.getItemId())) {
+            entity.setStartingAfter(lastBeforeYes.getItemId());
+            log.info("分页起点:前天最后一条itemId={}", lastBeforeYes.getItemId());
+        } else {
+            log.info("无前天数据,从头拉取三方交易");
+        }
+
+        // 2、划定昨天时间范围(秒级时间戳)
+        DateTime yesZero = DateUtil.beginOfDay(DateUtil.yesterday());
+        long yesStartTs = yesZero.getTime() / 1000;
+        DateTime todayZero = DateUtil.beginOfDay(new Date());
+        long yesEndTs = todayZero.getTime() / 1000;
+
         List<TransactionItemDto> result = new ArrayList<>();
+        List<String> existItemIds = recordByVaultId(config.getVaultId());
+        Set<String> existSet = new HashSet<>(existItemIds);
+
+        VaultTransaction pageData = query3Items(entity, config);
+
+        // 处理第一页
+        if (pageData.getList() != null && !pageData.getList().isEmpty()) {
+            List<TransactionItemDto> filterList = pageData.getList().stream()
+                    .filter(dto -> dto != null && dto.getId() != null && dto.getCreatedTimestamp() != null)
+                    // 只昨天数据 + 库不存在
+                    .filter(dto -> dto.getCreatedTimestamp() >= yesStartTs && dto.getCreatedTimestamp() < yesEndTs)
+                    .filter(dto -> !existSet.contains(dto.getId()))
+                    .collect(Collectors.toList());
+            result.addAll(filterList);
+
+            // 当前页最小时间早于昨天,后面都是更早数据,直接终止
+            long minTs = pageData.getList().stream()
+                    .map(TransactionItemDto::getCreatedTimestamp)
+                    .filter(Objects::nonNull)
+                    .mapToLong(Long::longValue)
+                    .min()
+                    .orElse(0L);
+            if (minTs < yesStartTs) {
+                return result;
+            }
+        }
+
+        // 循环分页拉取
+        while (Boolean.TRUE.equals(pageData.getHasMore())
+                && pageData.getList() != null
+                && !pageData.getList().isEmpty()) {
+
+            // 拿当前页最后一条id做下一页起点
+            String lastItemId = pageData.getList().get(pageData.getList().size() - 1).getId();
+            entity.setStartingAfter(lastItemId);
+
+            pageData = query3Items(entity, config);
+            if (pageData.getList() == null || pageData.getList().isEmpty()) {
+                break;
+            }
+
+            List<TransactionItemDto> filterList = pageData.getList().stream()
+                    .filter(dto -> dto != null && dto.getId() != null && dto.getCreatedTimestamp() != null)
+                    .filter(dto -> dto.getCreatedTimestamp() >= yesStartTs && dto.getCreatedTimestamp() < yesEndTs)
+                    .filter(dto -> !existSet.contains(dto.getId()))
+                    .collect(Collectors.toList());
+            result.addAll(filterList);
+
+            long minTs = pageData.getList().stream()
+                    .map(TransactionItemDto::getCreatedTimestamp)
+                    .filter(Objects::nonNull)
+                    .mapToLong(Long::longValue)
+                    .min()
+                    .orElse(0L);
+            if (minTs < yesStartTs) {
+                break;
+            }
+        }
+
+        return result;
+    }
+
+
+    /**
+     * 同步近半天(12小时)交易数据
+     * @param config 金库配置
+     * @return 待入库交易列表
+     * @throws Exception
+     */
+    public List<TransactionItemDto> queryWithFilterHalfDay(VaultodyConfig config) throws Exception {
+        VaultTransactionsEntity entity = new VaultTransactionsEntity();
+        entity.setVaultId(config.getVaultId());
+
+        // 时间范围:当前时间 -12小时 至 当前时间
+        DateTime now = DateUtil.date();
+        DateTime halfDayAgo = DateUtil.offsetHour(now, -12);
+        long startTs = halfDayAgo.getTime() / 1000;
+        long endTs = now.getTime() / 1000;
 
-        // 先获取数据库中已存在的item ID列表
+        List<TransactionItemDto> result = new ArrayList<>();
         List<String> existingItemIds = recordByVaultId(config.getVaultId());
         Set<String> existingIdSet = new HashSet<>(existingItemIds);
-        VaultTransaction vaultTransaction = query3Items(entity,config);
 
-        // 处理第一页数据
+        VaultTransaction vaultTransaction = query3Items(entity, config);
+
+        // 第一页过滤
         if (vaultTransaction.getList() != null && !vaultTransaction.getList().isEmpty()) {
-            List<TransactionItemDto> filteredList = filterExistingItems(vaultTransaction.getList(), existingIdSet);
+            List<TransactionItemDto> pageList = vaultTransaction.getList();
+            List<TransactionItemDto> filteredList = pageList.stream()
+                    .filter(item -> item != null && item.getId() != null && item.getCreatedTimestamp() != null)
+                    .filter(item -> !existingIdSet.contains(item.getId()))
+                    .filter(item -> item.getCreatedTimestamp() >= startTs && item.getCreatedTimestamp() <= endTs)
+                    .collect(Collectors.toList());
             result.addAll(filteredList);
+
+            // 本页最小时间早于12小时起点,后续都是更早数据,直接退出
+            long minPageTs = pageList.stream()
+                    .map(TransactionItemDto::getCreatedTimestamp)
+                    .filter(Objects::nonNull)
+                    .mapToLong(Long::longValue)
+                    .min()
+                    .orElse(0L);
+            if (minPageTs < startTs) {
+                return result;
+            }
         }
 
-        // 分页查询剩余数据
+        // 循环分页拉取
         while (Boolean.TRUE.equals(vaultTransaction.getHasMore())
                 && vaultTransaction.getList() != null
                 && !vaultTransaction.getList().isEmpty()) {
@@ -529,12 +666,26 @@ public class VaultodyServiceImpl implements VaultodyService {
             String lastId = vaultTransaction.getList().get(vaultTransaction.getList().size() - 1).getId();
             entity.setStartingAfter(lastId);
 
-            vaultTransaction = query3Items(entity,config);
+            vaultTransaction = query3Items(entity, config);
+            if (vaultTransaction.getList() == null || vaultTransaction.getList().isEmpty()) {
+                break;
+            }
+
+            List<TransactionItemDto> pageList = vaultTransaction.getList();
+            List<TransactionItemDto> filteredList = pageList.stream()
+                    .filter(item -> item != null && item.getId() != null && item.getCreatedTimestamp() != null)
+                    .filter(item -> !existingIdSet.contains(item.getId()))
+                    .filter(item -> item.getCreatedTimestamp() >= startTs && item.getCreatedTimestamp() <= endTs)
+                    .collect(Collectors.toList());
+            result.addAll(filteredList);
 
-            if (vaultTransaction.getList() != null && !vaultTransaction.getList().isEmpty()) {
-                List<TransactionItemDto> filteredList = filterExistingItems(vaultTransaction.getList(), existingIdSet);
-                result.addAll(filteredList);
-            } else {
+            long minPageTs = pageList.stream()
+                    .map(TransactionItemDto::getCreatedTimestamp)
+                    .filter(Objects::nonNull)
+                    .mapToLong(Long::longValue)
+                    .min()
+                    .orElse(0L);
+            if (minPageTs < startTs) {
                 break;
             }
         }

+ 8 - 0
crm-manager/src/main/resources/mapper/TransactionItemMapper.xml

@@ -47,5 +47,13 @@
         select * from transaction_item ti
         <include refid="transactionItemWhere"/>
         order by ti.created_timestamp desc
+        limit #{entity.page.offset},#{entity.page.row}
+    </select>
+
+    <select id="getBeforeYesterdayLast" resultType="com.crm.rely.backend.model.pojo.table.TransactionItemTable">
+        SELECT * FROM transaction_item
+        WHERE vault_id = #{vaultId}
+          AND DATE(FROM_UNIXTIME(created_timestamp)) = DATE(NOW() - INTERVAL 2 DAY)
+        ORDER BY id DESC LIMIT 1
     </select>
 </mapper>

+ 7 - 0
crm-model/pom.xml

@@ -76,5 +76,12 @@
             <artifactId>commons-lang3</artifactId>
             <version>3.8.1</version>
         </dependency>
+
+        <!-- Hutool 工具包 -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.22</version>
+        </dependency>
     </dependencies>
 </project>

+ 16 - 0
crm-model/src/main/java/com/crm/rely/backend/model/dto/vaultody/TransactionItemResultDto.java

@@ -0,0 +1,16 @@
+package com.crm.rely.backend.model.dto.vaultody;
+
+import com.crm.rely.backend.core.dto.base.PageDto;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class TransactionItemResultDto {
+    private PageDto page;
+    private List<TransactionItemSearchDto> list;
+    private BigDecimal totalSenderAmount;
+    private BigDecimal totalRecipientAmount;
+    private BigDecimal totalFeeAmount;
+}

+ 36 - 0
crm-model/src/main/java/com/crm/rely/backend/model/dto/vaultody/TransactionItemSearchDto.java

@@ -0,0 +1,36 @@
+package com.crm.rely.backend.model.dto.vaultody;
+
+import com.crm.rely.backend.core.pojo.BaseTable;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class TransactionItemSearchDto extends BaseTable {
+    private String requestId;
+    private String itemId;
+    private String vaultId;
+    private String transactionId;
+    private String status;
+    private Long createdTimestamp;
+
+    private String senderAddress;
+    private String senderIsVaultAddress;
+    private String senderAmountUnit;
+    private String senderAmount;
+    private String senderLabel;
+
+    private String recipientAddress;
+    private String recipientIsVaultAddress;
+    private String recipientAmountUnit;
+    private String recipientAmount;
+    private String recipientLabel;
+
+    private String blockchain;
+    private String minedInBlockHeight;
+    private String feeAmount;
+    private String feeAmountUnit;
+    private BigDecimal totalSenderAmount;
+    private BigDecimal totalRecipientAmount;
+    private BigDecimal totalFeeAmount;
+}