链上充值监听与自动划转资金流程实现

链上充值监听与自动划转资金流程实现

链上充值监听与自动划转是数字货币信用卡系统的核心环节,需要实时、准确地捕捉用户充值行为并自动完成资金归集。以下是完整实现方案,包括架构设计、核心流程和代码实现。

一、整体架构设计

链上充值流程

核心组件

  1. 充值地址生成服务:为每个用户生成唯一的充值地址(基于HD钱包派生)
  2. 区块监听服务:实时同步区块链数据,监控指定地址的交易
  3. 交易解析服务:验证交易合法性,提取关键信息(金额、发送方等)
  4. 充值确认服务:根据区块链确认数判断交易是否有效
  5. 资金划转服务:将到账资金划转到平台归集钱包或用户可用余额
  6. 通知服务:向用户推送充值到账信息

二、详细流程设计

1. 充值地址生成与关联

  • 基于用户ID和HD钱包路径,为每个用户生成唯一充值地址
  • 地址格式:支持多种区块链(BTC使用bech32,ETH使用十六进制等)
  • 地址与用户账户一对一绑定,存入数据库并关联用户ID

2. 区块监听流程

  • 连接区块链全节点或第三方API(如Infura、Alchemy)
  • 从最新区块开始监听,同时回溯检查历史区块(防止遗漏)
  • 对每个区块,解析所有交易,筛选涉及平台充值地址的交易
  • 采用增量同步策略,记录已处理的区块高度,避免重复处理

3. 交易验证与确认

  • 验证交易是否成功(区块确认数 >= 系统设定阈值,如ETH需要12个确认)
  • 验证交易金额是否满足最小充值限额
  • 检查交易是否已被处理(防止重复入账)
  • 计算实际到账金额(扣除区块链手续费后)

4. 资金划转与记账

  • 定时/自动将到账资金从用户充值地址划转到平台归集钱包(冷钱包)
  • 或直接增加用户账户可用余额(信用额度)
  • 生成充值记录和系统内转账记录,确保账目清晰
  • 触发后续业务流程(如自动激活卡片、提升额度等)
充值确认 → 本地事务(创建充值记录+写入消息表) → 定时任务发送消息 → MQ队列 → 消费消息(更新余额)
                                  ↓                ↓                ↓
                               状态跟踪        失败重试机制        消费重试+死信

三、核心代码实现

1. 充值地址生成服务

package com.digitalcredit.deposit.service;

import com.digitalcredit.user.entity.User;
import com.digitalcredit.user.service.UserService;
import com.digitalcredit.wallet.entity.DepositAddress;
import com.digitalcredit.wallet.entity.enums.BlockchainType;
import com.digitalcredit.wallet.repository.DepositAddressRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.web3j.crypto.*;
import org.web3j.utils.Numeric;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Service
public class DepositAddressService {

    @Autowired
    private DepositAddressRepository depositAddressRepository;
    
    @Autowired
    private UserService userService;
    
    // 平台根钱包(用于派生用户充值地址)
    private final Credentials platformRootCredentials;
    
    // 不同区块链的HD路径前缀
    private static final Map<BlockchainType, String> BIP44_PATH_PREFIXES = new HashMap<>();
    static {
        BIP44_PATH_PREFIXES.put(BlockchainType.BITCOIN, "m/44'/0'/0'/0/");
        BIP44_PATH_PREFIXES.put(BlockchainType.ETHEREUM, "m/44'/60'/0'/0/");
        BIP44_PATH_PREFIXES.put(BlockchainType.BSC, "m/44'/56'/0'/0/");
    }
    
    public DepositAddressService(String platformRootPrivateKey) {
        // 初始化平台根钱包
        this.platformRootCredentials = Credentials.create(platformRootPrivateKey);
    }
    
    /**
     * 为用户生成指定区块链的充值地址
     */
    @Transactional
    public DepositAddress generateDepositAddress(Long userId, BlockchainType blockchainType) {
        // 1. 验证用户
        User user = userService.getUserById(userId);
        if (user == null) {
            throw new IllegalArgumentException("User not found");
        }
        
        // 2. 检查用户是否已有该链的充值地址
        Optional<DepositAddress> existingAddress = depositAddressRepository
            .findByUserIdAndBlockchainType(userId, blockchainType);
        
        if (existingAddress.isPresent()) {
            return existingAddress.get();
        }
        
        // 3. 生成用户唯一索引(可基于用户ID或自增序列)
        long userIndex = generateUserIndex(userId);
        
        // 4. 构建BIP44路径
        String path = BIP44_PATH_PREFIXES.get(blockchainType) + userIndex;
        
        // 5. 从根钱包派生出用户充值地址的私钥
        ECKeyPair userKeyPair = deriveKeyPairFromPath(platformRootCredentials.getEcKeyPair(), path);
        String address;
        
        // 6. 根据不同区块链生成地址
        switch (blockchainType) {
            case ETHEREUM:
            case BSC:
                address = "0x" + Keys.getAddress(userKeyPair);
                break;
            case BITCOIN:
                // 比特币地址生成逻辑(使用bitcoinj等库)
                address = generateBitcoinAddress(userKeyPair);
                break;
            default:
                throw new UnsupportedOperationException("Unsupported blockchain type");
        }
        
        // 7. 保存充值地址(私钥加密存储)
        DepositAddress depositAddress = new DepositAddress();
        depositAddress.setUserId(userId);
        depositAddress.setBlockchainType(blockchainType);
        depositAddress.setAddress(address);
        depositAddress.setDerivationPath(path);
        // 加密存储私钥
        depositAddress.setEncryptedPrivateKey(encryptPrivateKey(
            Numeric.toHexStringWithPrefix(userKeyPair.getPrivateKey())));
        depositAddress.setCreatedAt(System.currentTimeMillis());
        
        return depositAddressRepository.save(depositAddress);
    }
    
    /**
     * 根据BIP44路径从父密钥派生子密钥
     */
    private ECKeyPair deriveKeyPairFromPath(ECKeyPair parent, String path) {
        String[] pathElements = path.split("/");
        ECKeyPair currentKeyPair = parent;
        
        for (String element : pathElements) {
            if (element.equals("m")) continue;
            
            boolean hardened = element.endsWith("'");
            int index = Integer.parseInt(hardened ? element.substring(0, element.length() - 1) : element);
            
            if (hardened) {
                index += 0x80000000; // 强化派生索引偏移
            }
            
            // 使用BIP32派生算法
            currentKeyPair = HDKeyGenerator.deriveChildKey(currentKeyPair, index);
        }
        
        return currentKeyPair;
    }
    
    /**
     * 生成用户唯一索引
     */
    private long generateUserIndex(Long userId) {
        // 可以使用userId的哈希或数据库自增序列
        return Math.abs(userId.hashCode() % 1000000);
    }
    
    /**
     * 加密私钥(生产环境使用AES-256加密)
     */
    private String encryptPrivateKey(String privateKey) {
        // 实际实现应使用安全的加密算法,密钥存储在安全的密钥管理服务中
        return EncryptionUtil.encrypt(privateKey, System.getenv("PRIVATE_KEY_ENCRYPTION_KEY"));
    }
    
    /**
     * 获取用户的充值地址
     */
    public String getUserDepositAddress(Long userId, BlockchainType blockchainType) {
        DepositAddress address = depositAddressRepository
            .findByUserIdAndBlockchainType(userId, blockchainType)
            .orElseThrow(() -> new IllegalArgumentException("No deposit address found for user and blockchain type"));
        
        return address.getAddress();
    }
    
    /**
     * 生成比特币地址(简化实现)
     */
    private String generateBitcoinAddress(ECKeyPair keyPair) {
        // 实际项目中使用bitcoinj等专业库实现
        // 这里仅作示例
        return BitcoinAddressGenerator.generateFromKeyPair(keyPair, false);
    }
}

2. 区块监听与交易处理服务

package com.digitalcredit.blockchain.service;

import com.digitalcredit.blockchain.config.BlockchainNodeConfig;
import com.digitalcredit.blockchain.entity.ChainTransaction;
import com.digitalcredit.blockchain.entity.enums.TransactionStatus;
import com.digitalcredit.blockchain.repository.ChainTransactionRepository;
import com.digitalcredit.deposit.service.DepositProcessingService;
import com.digitalcredit.wallet.entity.DepositAddress;
import com.digitalcredit.wallet.repository.DepositAddressRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.protocol.core.methods.response.EthBlock;
import org.web3j.protocol.core.methods.response.EthBlockNumber;
import org.web3j.protocol.http.HttpService;

import javax.annotation.PostConstruct;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

@Service
public class FaultTolerantBlockchainListener {
    private static final Logger logger = LoggerFactory.getLogger(FaultTolerantBlockchainListener.class);
    
    // 每个链的节点配置
    private final Map<String, List<BlockchainNodeConfig>> nodeConfigs = new ConcurrentHashMap<>();
    
    // 当前活跃的Web3j客户端
    private final Map<String, Web3j> activeWeb3jClients = new ConcurrentHashMap<>();
    
    // 节点健康状态
    private final Map<String, Map<String, Boolean>> nodeHealthStatus = new ConcurrentHashMap<>();
    
    // 平台充值地址缓存(小写)
    private final Set<String> platformAddresses = ConcurrentHashMap.newKeySet();
    
    // 区块处理线程池
    private final ExecutorService blockProcessingExecutor = new ThreadPoolExecutor(
        4, 16, 5, TimeUnit.MINUTES,
        new LinkedBlockingQueue<>(1000),
        new ThreadFactory() {
            private int counter = 0;
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "block-processor-" + counter++);
                thread.setDaemon(true);
                return thread;
            }
        },
        new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时让提交者执行,防止任务丢失
    );
    
    @Autowired
    private List<BlockchainNodeConfig> allNodeConfigs;
    
    @Autowired
    private DepositAddressRepository depositAddressRepository;
    
    @Autowired
    private ChainTransactionRepository transactionRepository;
    
    @Autowired
    private DepositProcessingService depositProcessingService;
    
    @Autowired
    private BlockchainNodeHealthChecker nodeHealthChecker;
    
    // 上次处理的区块高度
    private final Map<String, BigInteger> lastProcessedBlock = new ConcurrentHashMap<>();
    
    // 确认数阈值配置
    private final Map<String, Integer> confirmationThresholds = new HashMap<>();
    {
        confirmationThresholds.put("ETH", 12);
        confirmationThresholds.put("BSC", 12);
        confirmationThresholds.put("BTC", 6);
        confirmationThresholds.put("LTC", 6);
    }
    
    @PostConstruct
    public void initialize() {
        // 1. 按链类型分组节点配置
        for (BlockchainNodeConfig config : allNodeConfigs) {
            nodeConfigs.computeIfAbsent(config.getChainType(), k -> new ArrayList<>())
                      .add(config);
            nodeHealthStatus.computeIfAbsent(config.getChainType(), k -> new ConcurrentHashMap<>())
                           .put(config.getNodeUrl(), true);
        }
        
        // 2. 初始化活跃客户端
        initializeActiveClients();
        
        // 3. 加载充值地址缓存
        refreshAddressCache();
        
        // 4. 加载上次处理的区块高度
        loadLastProcessedBlocks();
        
        // 5. 启动节点健康检查
        nodeHealthChecker.start();
        
        logger.info("Blockchain listener initialized with {} chains", nodeConfigs.size());
    }
    
    /**
     * 初始化活跃客户端(选择健康的节点)
     */
    private void initializeActiveClients() {
        for (String chainType : nodeConfigs.keySet()) {
            try {
                Web3j client = getHealthyWeb3jClient(chainType);
                if (client != null) {
                    activeWeb3jClients.put(chainType, client);
                    logger.info("Initialized active client for chain: {}", chainType);
                }
            } catch (Exception e) {
                logger.error("Failed to initialize client for chain: {}", chainType, e);
            }
        }
    }
    
    /**
     * 获取健康的Web3j客户端(带故障转移)
     */
    private Web3j getHealthyWeb3jClient(String chainType) {
        List<BlockchainNodeConfig> configs = nodeConfigs.getOrDefault(chainType, Collections.emptyList());
        if (configs.isEmpty()) {
            logger.warn("No node configs for chain: {}", chainType);
            return null;
        }
        
        // 按优先级排序,优先选择主节点
        configs.sort(Comparator.comparingInt(BlockchainNodeConfig::getPriority));
        
        // 尝试连接健康节点
        for (BlockchainNodeConfig config : configs) {
            if (nodeHealthStatus.get(chainType).getOrDefault(config.getNodeUrl(), false)) {
                try {
                    Web3j client = Web3j.build(new HttpService(config.getNodeUrl()));
                    // 测试连接
                    client.ethBlockNumber().sendAsync().get(5, TimeUnit.SECONDS);
                    return client;
                } catch (Exception e) {
                    logger.warn("Node {} for chain {} is unhealthy: {}", 
                              config.getNodeUrl(), chainType, e.getMessage());
                    nodeHealthStatus.get(chainType).put(config.getNodeUrl(), false);
                }
            }
        }
        
        logger.error("No healthy nodes available for chain: {}", chainType);
        return null;
    }
    
    /**
     * 定时刷新地址缓存(每30分钟)
     */
    @Scheduled(fixedRate = 30 * 60 * 1000)
    public void refreshAddressCache() {
        try {
            long start = System.currentTimeMillis();
            List<String> addresses = depositAddressRepository.findAllActive()
                .stream()
                .map(DepositAddress::getAddress)
                .map(String::toLowerCase)
                .collect(Collectors.toList());
            
            platformAddresses.clear();
            platformAddresses.addAll(addresses);
            logger.info("Refreshed deposit addresses cache, count: {}, time: {}ms",
                      addresses.size(), System.currentTimeMillis() - start);
        } catch (Exception e) {
            logger.error("Failed to refresh address cache", e);
        }
    }
    
    /**
     * 加载上次处理的区块高度
     */
    private void loadLastProcessedBlocks() {
        for (String chainType : nodeConfigs.keySet()) {
            BigInteger lastBlock = transactionRepository.findMaxBlockNumberByChain(chainType)
                .orElse(BigInteger.ZERO);
            lastProcessedBlock.put(chainType, lastBlock);
            logger.info("Loaded last processed block for {}: {}", chainType, lastBlock);
        }
    }
    
    /**
     * 定时监听新区块(每5秒)
     */
    @Scheduled(fixedRate = 5000)
    public void listenForNewBlocks() {
        for (String chainType : new ArrayList<>(nodeConfigs.keySet())) {
            try {
                processChain(chainType);
            } catch (Exception e) {
                logger.error("Error processing chain: {}", chainType, e);
                // 尝试切换客户端
                Web3j newClient = getHealthyWeb3jClient(chainType);
                if (newClient != null) {
                    activeWeb3jClients.put(chainType, newClient);
                    logger.info("Switched to new client for chain: {}", chainType);
                }
            }
        }
    }
    
    /**
     * 处理单个链的区块
     */
    private void processChain(String chainType) throws Exception {
        Web3j web3j = activeWeb3jClients.get(chainType);
        if (web3j == null) {
            logger.warn("No active client for chain: {}", chainType);
            return;
        }
        
        // 获取最新区块高度
        EthBlockNumber blockNumberResp = web3j.ethBlockNumber().send();
        BigInteger latestBlock = blockNumberResp.getBlockNumber();
        
        // 获取上次处理的区块高度
        BigInteger startBlock = lastProcessedBlock.getOrDefault(chainType, BigInteger.ZERO);
        
        // 计算需要处理的区块范围
        if (latestBlock.compareTo(startBlock) <= 0) {
            // 没有新区块,检查确认中的交易
            checkPendingTransactions(chainType, latestBlock);
            return;
        }
        
        // 限制每次处理的区块数量,防止过载
        BigInteger endBlock = startBlock.add(BigInteger.valueOf(10));
        if (endBlock.compareTo(latestBlock) > 0) {
            endBlock = latestBlock;
        }
        
        logger.info("Processing chain {}: blocks {} to {}", chainType, startBlock, endBlock);
        
        // 并行处理区块
        for (BigInteger blockNum = startBlock.add(BigInteger.ONE); 
             blockNum.compareTo(endBlock) <= 0; 
             blockNum = blockNum.add(BigInteger.ONE)) {
            processSingleBlockAsync(chainType, web3j, blockNum);
        }
        
        // 更新最后处理的区块高度
        lastProcessedBlock.put(chainType, endBlock);
    }
    
    /**
     * 异步处理单个区块
     */
    @Async("blockProcessingExecutor")
    @Retryable(
        value = {Exception.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2)
    )
    public void processSingleBlockAsync(String chainType, Web3j web3j, BigInteger blockNumber) {
        try {
            processSingleBlock(chainType, web3j, blockNumber);
        } catch (Exception e) {
            logger.error("Failed to process block {} on chain {} after retries", 
                      blockNumber, chainType, e);
            // 记录失败的区块,供后续手动处理
            transactionRepository.recordFailedBlock(chainType, blockNumber, e.getMessage());
        }
    }
    
    /**
     * 处理单个区块
     */
    private void processSingleBlock(String chainType, Web3j web3j, BigInteger blockNumber) throws Exception {
        EthBlock block = web3j.ethGetBlockByNumber(
            DefaultBlockParameter.valueOf(blockNumber), 
            true // 包含交易详情
        ).send();
        
        if (block.getBlock() == null) {
            logger.warn("Block {} not found on chain {}", blockNumber, chainType);
            return;
        }
        
        // 处理区块中的交易
        for (EthBlock.TransactionResult<?> txResult : block.getBlock().getTransactions()) {
            EthBlock.TransactionObject tx = (EthBlock.TransactionObject) txResult.get();
            
            // 检查是否是平台地址的入金交易
            if (tx.getTo() != null && platformAddresses.contains(tx.getTo().toLowerCase())) {
                processDepositTransaction(chainType, tx, blockNumber);
            }
        }
        
        logger.debug("Processed block {} on chain {}, transactions: {}",
                  blockNumber, chainType, block.getBlock().getTransactions().size());
    }
    
    /**
     * 处理充值交易
     */
    private void processDepositTransaction(String chainType, 
                                          EthBlock.TransactionObject tx, 
                                          BigInteger blockNumber) {
        // 检查交易是否已处理
        if (transactionRepository.existsByTxHashAndChainType(tx.getHash(), chainType)) {
            return;
        }
        
        // 创建交易记录
        ChainTransaction transaction = new ChainTransaction();
        transaction.setTxHash(tx.getHash());
        transaction.setChainType(chainType);
        transaction.setFromAddress(tx.getFrom());
        transaction.setToAddress(tx.getTo());
        transaction.setAmount(tx.getValue());
        transaction.setBlockNumber(blockNumber);
        transaction.setGasPrice(tx.getGasPrice());
        transaction.setGasUsed(tx.getGas());
        transaction.setInputData(tx.getInput());
        transaction.setTimestamp(System.currentTimeMillis());
        
        // 初始状态:待确认
        int requiredConfirmations = confirmationThresholds.getOrDefault(chainType, 12);
        transaction.setStatus(TransactionStatus.PENDING_CONFIRMATION);
        transaction.setRequiredConfirmations(requiredConfirmations);
        transaction.setCurrentConfirmations(BigInteger.ZERO);
        
        transactionRepository.save(transaction);
        logger.info("Found new deposit transaction {} on chain {}, amount: {}",
                  tx.getHash(), chainType, tx.getValue());
    }
    
    /**
     * 检查待确认的交易
     */
    private void checkPendingTransactions(String chainType, BigInteger latestBlock) {
        try {
            List<ChainTransaction> pendingTxs = transactionRepository
                .findByChainTypeAndStatus(chainType, TransactionStatus.PENDING_CONFIRMATION);
            
            for (ChainTransaction tx : pendingTxs) {
                // 计算当前确认数
                BigInteger confirmations = latestBlock.subtract(tx.getBlockNumber());
                tx.setCurrentConfirmations(confirmations);
                
                // 检查是否达到确认阈值
                if (confirmations.compareTo(BigInteger.valueOf(tx.getRequiredConfirmations())) >= 0) {
                    tx.setStatus(TransactionStatus.CONFIRMED);
                    transactionRepository.save(tx);
                    
                    // 提交给充值处理服务
                    depositProcessingService.processConfirmedDeposit(tx);
                } else if (System.currentTimeMillis() - tx.getTimestamp() > 24 * 60 * 60 * 1000) {
                    // 超过24小时未确认,标记为失败
                    tx.setStatus(TransactionStatus.CONFIRMATION_FAILED);
                    transactionRepository.save(tx);
                    logger.warn("Transaction {} on chain {} failed to confirm within 24h",
                              tx.getTxHash(), chainType);
                } else {
                    transactionRepository.save(tx);
                }
            }
        } catch (Exception e) {
            logger.error("Error checking pending transactions for chain {}", chainType, e);
        }
    }
}

3. 充值处理与资金划转服务(RocketMQ)

0). 数据库设计

-- 充值记录表
CREATE TABLE deposit_record (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    tx_hash VARCHAR(66) NOT NULL COMMENT '区块链交易哈希',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    chain_type VARCHAR(20) NOT NULL COMMENT '区块链类型(ETH/BSC等)',
    deposit_address VARCHAR(66) NOT NULL COMMENT '充值地址',
    amount DECIMAL(30,18) NOT NULL COMMENT '充值金额',
    block_number BIGINT NOT NULL COMMENT '区块高度',
    status VARCHAR(20) NOT NULL COMMENT '状态(PENDING/SUCCESS/FAILED)',
    failure_reason TEXT COMMENT '失败原因',
    created_at BIGINT NOT NULL COMMENT '创建时间戳',
    completed_at BIGINT COMMENT '完成时间戳',
    UNIQUE KEY uk_tx_hash (tx_hash),
    KEY idx_user_id (user_id),
    KEY idx_status_create_time (status, created_at)
) ENGINE=InnoDB COMMENT='充值记录表';

-- 本地消息表
CREATE TABLE local_message (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    business_key VARCHAR(66) NOT NULL COMMENT '业务唯一标识(如交易哈希)',
    message_type VARCHAR(20) NOT NULL COMMENT '消息类型(DEPOSIT_ARRIVAL/FUND_COLLECTION)',
    topic VARCHAR(50) NOT NULL COMMENT 'MQ主题',
    content TEXT NOT NULL COMMENT '消息内容(JSON)',
    status VARCHAR(20) NOT NULL COMMENT '状态(PENDING/SENDING/SENT/FAILED)',
    send_count INT NOT NULL DEFAULT 0 COMMENT '发送次数',
    max_retry_count INT NOT NULL DEFAULT 3 COMMENT '最大重试次数',
    next_send_time BIGINT NOT NULL COMMENT '下次发送时间戳',
    last_error TEXT COMMENT '最后错误信息',
    created_at BIGINT NOT NULL COMMENT '创建时间戳',
    updated_at BIGINT COMMENT '更新时间戳',
    UNIQUE KEY uk_business_key_type (business_key, message_type),
    KEY idx_status_next_send (status, next_send_time)
) ENGINE=InnoDB COMMENT='本地消息表';

-- 用户账户表(简化)
CREATE TABLE user_account (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL UNIQUE COMMENT '用户ID',
    available_balance DECIMAL(30,18) NOT NULL DEFAULT 0 COMMENT '可用余额',
    total_balance DECIMAL(30,18) NOT NULL DEFAULT 0 COMMENT '总余额',
    updated_at BIGINT NOT NULL COMMENT '更新时间戳'
) ENGINE=InnoDB COMMENT='用户账户表';

1). 消息与业务实体类

package com.digitalcredit.message.entity;

import lombok.Data;

import javax.persistence.*;
import java.math.BigDecimal;

@Data
@Entity
@Table(name = "local_message")
public class LocalMessage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "business_key", nullable = false, length = 66)
    private String businessKey; // 交易哈希

    @Column(name = "message_type", nullable = false, length = 20)
    @Enumerated(EnumType.STRING)
    private MessageType messageType; // 消息类型

    @Column(name = "topic", nullable = false, length = 50)
    private String topic; // MQ主题

    @Column(name = "content", nullable = false, columnDefinition = "TEXT")
    private String content; // 消息内容(JSON)

    @Column(name = "status", nullable = false, length = 20)
    @Enumerated(EnumType.STRING)
    private MessageStatus status; // 消息状态

    @Column(name = "send_count", nullable = false)
    private Integer sendCount = 0; // 发送次数

    @Column(name = "max_retry_count", nullable = false)
    private Integer maxRetryCount = 3; // 最大重试次数

    @Column(name = "next_send_time", nullable = false)
    private Long nextSendTime; // 下次发送时间戳

    @Column(name = "last_error", columnDefinition = "TEXT")
    private String lastError; // 最后错误信息

    @Column(name = "created_at", nullable = false)
    private Long createdAt; // 创建时间戳

    @Column(name = "updated_at")
    private Long updatedAt; // 更新时间戳

    // 消息类型枚举
    public enum MessageType {
        DEPOSIT_ARRIVAL, // 资金到账
        FUND_COLLECTION  // 资金归集
    }

    // 消息状态枚举
    public enum MessageStatus {
        PENDING,   // 待发送
        SENDING,   // 发送中
        SENT,      // 已发送
        FAILED     // 发送失败
    }
}


package com.digitalcredit.deposit.entity;

import lombok.Data;

import javax.persistence.*;
import java.math.BigDecimal;

@Data
@Entity
@Table(name = "deposit_record")
public class DepositRecord {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "tx_hash", nullable = false, length = 66, unique = true)
    private String txHash; // 区块链交易哈希

    @Column(name = "user_id", nullable = false)
    private Long userId; // 用户ID

    @Column(name = "chain_type", nullable = false, length = 20)
    private String chainType; // 区块链类型

    @Column(name = "deposit_address", nullable = false, length = 66)
    private String depositAddress; // 充值地址

    @Column(name = "amount", nullable = false, precision = 30, scale = 18)
    private BigDecimal amount; // 充值金额

    @Column(name = "block_number", nullable = false)
    private Long blockNumber; // 区块高度

    @Column(name = "status", nullable = false, length = 20)
    @Enumerated(EnumType.STRING)
    private DepositStatus status; // 状态

    @Column(name = "failure_reason", columnDefinition = "TEXT")
    private String failureReason; // 失败原因

    @Column(name = "created_at", nullable = false)
    private Long createdAt; // 创建时间戳

    @Column(name = "completed_at")
    private Long completedAt; // 完成时间戳

    // 充值状态枚举
    public enum DepositStatus {
        PENDING,   // 待处理
        SUCCESS,   // 成功
        FAILED     // 失败
    }
}


    

2). 资金到账消息消费者

package com.digitalcredit.mq.consumer;

import com.alibaba.fastjson.JSON;
import com.digitalcredit.account.service.AccountService;
import com.digitalcredit.deposit.entity.DepositRecord;
import com.digitalcredit.deposit.entity.enums.DepositStatus;
import com.digitalcredit.deposit.repository.DepositRecordRepository;
import com.digitalcredit.mq.message.DepositMessage;
import org.apache.rocketmq.client.consumer.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Component
public class DepositMessageListener implements MessageListenerConcurrently {
    private static final Logger logger = LoggerFactory.getLogger(DepositMessageListener.class);

    @Autowired
    private AccountService accountService;

    @Autowired
    private DepositRecordRepository depositRecordRepository;

    /**
     * 处理资金划转消息
     */
    @Override
    @Transactional
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        for (MessageExt msg : msgs) {
            try {
                String txHash = msg.getKeys();
                logger.info("开始处理资金划转消息,txHash: {}, 重试次数: {}", txHash, msg.getReconsumeTimes());

                // 1. 解析消息
                DepositMessage message = JSON.parseObject(
                    new String(msg.getBody(), "UTF-8"), 
                    DepositMessage.class
                );

                // 2. 防重复处理(检查是否已处理)
                Optional<DepositRecord> recordOpt = depositRecordRepository.findByTxHash(txHash);
                if (recordOpt.isEmpty()) {
                    logger.error("未找到对应的充值记录,txHash: {}", txHash);
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }

                DepositRecord record = recordOpt.get();
                if (record.getStatus() == DepositStatus.SUCCESS) {
                    logger.info("消息已处理,无需重复处理,txHash: {}", txHash);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }

                // 3. 执行资金划转(增加用户余额)
                accountService.increaseBalance(
                    message.getUserId(),
                    message.getAmount(),
                    "DEPOSIT",
                    txHash
                );

                // 4. 更新充值记录状态
                record.setStatus(DepositStatus.SUCCESS);
                record.setCompletedAt(System.currentTimeMillis());
                depositRecordRepository.save(record);

                logger.info("资金划转处理成功,txHash: {}", txHash);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

            } catch (Exception e) {
                logger.error("资金划转处理失败,msgId: {}", msg.getMsgId(), e);
                
                // 判断是否达到最大重试次数
                if (msg.getReconsumeTimes() >= 5) {
                    // 记录失败原因,后续人工处理
                    String txHash = msg.getKeys();
                    updateDepositRecordToFailed(txHash, e.getMessage());
                    
                    logger.warn("消息达到最大重试次数,将进入死信队列,txHash: {}", txHash);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; // 不再重试,进入死信队列
                }
                
                // 未达最大重试次数,返回重试
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        }
        
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }

    /**
     * 更新充值记录为失败状态
     */
    private void updateDepositRecordToFailed(String txHash, String reason) {
        try {
            Optional<DepositRecord> recordOpt = depositRecordRepository.findByTxHash(txHash);
            if (recordOpt.isPresent()) {
                DepositRecord record = recordOpt.get();
                record.setStatus(DepositStatus.FAILED);
                record.setFailureReason(reason);
                record.setCompletedAt(System.currentTimeMillis());
                depositRecordRepository.save(record);
            }
        } catch (Exception e) {
            logger.error("更新充值记录为失败状态失败,txHash: {}", txHash, e);
        }
    }
}
    

四、关键技术点说明

1. 区块链节点连接策略

  • 主备节点机制:同时连接多个节点,当主节点故障时自动切换到备用节点
  • 连接池管理:维护长期连接,减少握手开销
  • 超时重试:设置合理的超时时间和重试策略,确保网络波动时的稳定性

2. 交易去重与幂等性保障

  • 使用交易哈希作为唯一标识,确保每笔交易只处理一次
  • 数据库唯一索引:在充值记录表中对交易哈希和链类型创建唯一索引
  • 状态机设计:明确的状态流转(PENDING → CONFIRMED/INVALID/ERROR),避免重复处理

3. 性能优化措施

  • 充值地址缓存:将平台所有充值地址加载到内存,减少数据库查询
  • 批量处理:区块处理采用批量方式,提高效率
  • 异步处理:交易解析和资金划转采用异步方式,避免阻塞监听线程
  • 分区表:充值记录表按时间分区,提高查询效率

4. 容错与恢复机制

  • 断点续传:记录已处理的区块高度,服务重启后从断点继续处理
  • 重试队列:处理失败的交易加入重试队列,定时重试
  • 监控告警:关键指标(区块同步延迟、未确认充值数量)监控和告警
  • 手动干预接口:提供手动处理异常充值的接口

五、安全措施

  1. 私钥安全

    • 充值地址私钥加密存储(AES-256)
    • 加密密钥通过KMS(密钥管理服务)管理
    • 敏感操作(如资金划转)需要多重签名
  2. 交易验证

    • 多重验证:不仅验证地址,还验证金额和交易状态
    • 防重放攻击:检查交易是否属于当前链
  3. 异常监控

    • 大额充值预警:超过阈值的充值触发人工审核
    • 异常地址监控:对黑名单地址的转账进行拦截
    • 频率限制:监控同一地址的频繁转账行为

六、总结

链上充值监听与自动划转系统需要兼顾实时性、准确性和安全性。通过合理的架构设计和技术选型,可以实现高效、可靠的充值处理流程。

核心要点:

  • 采用HD钱包技术为每个用户生成唯一充值地址
  • 实时区块监听与交易解析,确保不遗漏任何充值
  • 基于区块确认数的交易有效性验证机制
  • 完善的异常处理和容错恢复机制
  • 严格的安全措施保护用户资金安全

实际部署时,应根据支持的区块链类型和用户规模进行水平扩展,确保系统在高并发场景下的稳定性和处理能力。

posted @ 2025-07-28 18:01  ffffox  阅读(74)  评论(0)    收藏  举报