qt+c++编写的监听链上钱包转账信息适合做USDT支付通道.本站的支付通道就是基于这么做的。


有几个注意的地方:

申请波场apikey否则查询会有诸多限制。

付款金额带小数,否则容易张冠李戴

超时未支付的订单不处理,代码逻辑很清楚,都加了详细的备注说明,可以参考代码自己扩展需要的功能,反正那些usdt支付平台都是这种方式。

初始化

// 初始化 Tron 监控
TronMonitor tronMonitor;
tronMonitor.setMysql(&mysql);
tronMonitor.setRedis(&redis);
QJsonObject tronCfg = config["tron"].toObject();
tronMonitor.setApiKey(tronCfg["api_key"].toString());
tronMonitor.setReceiveAddress(tronCfg["receive_address"].toString());
tronMonitor.setPollInterval(tronCfg["poll_interval"].toInt(10));
tronMonitor.start();

函数实现

#include "TronMonitor.h"
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDateTime>
#include <QDebug>

TronMonitor::TronMonitor(QObject* parent)
    : QObject(parent)
{
    m_net = new QNetworkAccessManager(this);
    m_timer = new QTimer(this);

    // 确保信号槽连接成功
    bool connected1 = connect(m_timer, &QTimer::timeout, this, &TronMonitor::onPoll);
    bool connected2 = connect(m_net, &QNetworkAccessManager::finished, this, &TronMonitor::onReplyFinished);

    if (!connected1 || !connected2) {
        qWarning() << "[TronMonitor] 信号槽连接失败!";
    }
}

void TronMonitor::start()
{
    if (m_receiveAddress.isEmpty()) {
        emit logMessage("[TronMonitor] ❌ 未配置收款地址,跳过启动");
        return;
    }
    if (m_apiKey.isEmpty()) {
        emit logMessage("[TronMonitor] ❌ 未配置 API Key,跳过启动");
        return;
    }

    // 从 Redis 加载已处理的 tx_hash 列表(防重启后重复处理)
    if (m_redis && m_redis->isConnected()) {
        QStringList hashes = m_redis->lrange("tron:processed_txs", 0, -1);
        for (const QString& hash : hashes) {
            m_processedTxHashes.insert(hash);
        }
        emit logMessage(QString("[TronMonitor] 📦 从 Redis 加载 %1 个已处理交易").arg(hashes.size()));
    }
    else {
        emit logMessage("[TronMonitor] ️ Redis 未连接,无法加载历史交易记录");
    }

    m_timer->start(m_pollInterval * 1000);
    emit logMessage(QString("[TronMonitor] ✅ 已启动,轮询间隔 %1 秒,监听地址: %2")
        .arg(m_pollInterval).arg(m_receiveAddress));

    // 立即执行一次
    onPoll();
}

void TronMonitor::stop()
{
    m_timer->stop();
    emit logMessage("[TronMonitor] 已停止");
}

void TronMonitor::onPoll()
{
    if (m_pendingReply) {
        emit logMessage("[TronMonitor] ⚠️ 上一个请求尚未完成,跳过本次轮询");
        return;
    }

    // 计算 10 分钟前的时间戳(毫秒)
    qint64 minTimestamp = (QDateTime::currentSecsSinceEpoch() - 600) * 1000;
    QDateTime minTime = QDateTime::fromMSecsSinceEpoch(minTimestamp);

    emit logMessage(QString("[TronMonitor] 🔍 开始查询链上交易... (范围: %1 ~ 现在)")
        .arg(minTime.toString("hh:mm:ss")));

    // 构建 TronGrid API URL
    QString url = QString("https://api.trongrid.io/v1/accounts/%1/transactions/trc20"
        "?limit=100&only_confirmed=true&min_timestamp=%2&contract_address=%3")
        .arg(m_receiveAddress)
        .arg(QString::number(minTimestamp))
        .arg(USDT_CONTRACT);

    emit logMessage(QString("[TronMonitor]  API URL: %1").arg(url));
    emit logMessage(QString("[TronMonitor]  API Key: %1...%2")
        .arg(m_apiKey.left(8)).arg(m_apiKey.right(4)));

    emit logMessage(QString("[TronMonitor]  API URL: %1").arg(url));
    emit logMessage(QString("[TronMonitor]  API Key 长度: %1").arg(m_apiKey.length()));
    emit logMessage(QString("[TronMonitor] 🔑 API Key 完整值: [%1]").arg(m_apiKey));
    emit logMessage(QString("[TronMonitor] 🔑 API Key HEX: %1")
        .arg(QString::fromLatin1(m_apiKey.toUtf8().toHex())));

    QNetworkRequest req{ QUrl{url} };
    req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
    req.setRawHeader("TRON-PRO-API-KEY", m_apiKey.toUtf8());

    // 忽略 SSL 证书验证
    QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
    sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
    req.setSslConfiguration(sslConfig);

    // 发送请求并保存 reply 指针
    m_pendingReply = m_net->get(req);

    // 连接 SSL 错误信号
    connect(m_pendingReply, &QNetworkReply::sslErrors, this, [this](const QList<QSslError>& errors) {
        for (const QSslError& err : errors) {
            emit logMessage(QString("[TronMonitor] ️ SSL 错误: %1").arg(err.errorString()));
        }
        if (m_pendingReply) {
            m_pendingReply->ignoreSslErrors();
        }
        });

    // 设置超时(5秒)
    QTimer::singleShot(5000, this, [this]() {
        if (m_pendingReply && m_pendingReply->isRunning()) {
            emit logMessage("[TronMonitor] ❌ 请求超时(5秒),强制取消");
            m_pendingReply->abort();
            m_pendingReply = nullptr;
        }
        });
}

void TronMonitor::onReplyFinished(QNetworkReply* reply)
{
    // 清空 pending reply
    if (m_pendingReply == reply) {
        m_pendingReply = nullptr;
    }

    if (!reply) {
        emit logMessage("[TronMonitor] ❌ reply 为空");
        return;
    }

    qint64 startTime = QDateTime::currentMSecsSinceEpoch();
    int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    QByteArray rawData = reply->readAll();

    emit logMessage(QString("[TronMonitor] 📡 HTTP 状态码: %1").arg(httpStatus));
    emit logMessage(QString("[TronMonitor] 📦 响应大小: %1 bytes").arg(rawData.size()));

    if (reply->error() != QNetworkReply::NoError) {
        emit logMessage(QString("[TronMonitor] ❌ API 请求失败: %1 (HTTP: %2, 耗时: %3ms)")
            .arg(reply->errorString())
            .arg(httpStatus)
            .arg(QDateTime::currentMSecsSinceEpoch() - startTime));

        if (!rawData.isEmpty()) {
            emit logMessage(QString("[TronMonitor]  响应内容: %1").arg(QString::fromUtf8(rawData)));
        }

        reply->deleteLater();
        return;
    }

    if (rawData.isEmpty()) {
        emit logMessage("[TronMonitor] ⚠️ 响应内容为空");
        reply->deleteLater();
        return;
    }

    // 打印响应内容前 500 字符用于调试
    QString preview = QString::fromUtf8(rawData.left(500));
    if (rawData.size() > 500) preview += "...";
    emit logMessage(QString("[TronMonitor] 📄 响应预览: %1").arg(preview));

    QJsonDocument doc = QJsonDocument::fromJson(rawData);
    QJsonObject root = doc.object();

    if (!root["success"].toBool()) {
        emit logMessage(QString("[TronMonitor] ❌ API 返回失败: %1 (耗时: %2ms)")
            .arg(root["Error"].toString())
            .arg(QDateTime::currentMSecsSinceEpoch() - startTime));
        reply->deleteLater();
        return;
    }

    QJsonArray txs = root["data"].toArray();
    qint64 elapsed = QDateTime::currentMSecsSinceEpoch() - startTime;

    if (txs.isEmpty()) {
        emit logMessage(QString("[TronMonitor] ✅ 无新交易 (耗时: %1ms)").arg(elapsed));
        reply->deleteLater();
        return;
    }

    emit logMessage(QString("[TronMonitor] 📊 收到 %1 笔交易 (耗时: %2ms)")
        .arg(txs.size()).arg(elapsed));

    processTransactions(txs);
    reply->deleteLater();
}
void TronMonitor::processTransactions(const QJsonArray& txs)
{
    int matchedCount = 0;
    int skippedCount = 0;
    int totalChecked = txs.size();

    emit logMessage(QString("[TronMonitor] 🔄 开始处理 %1 笔交易...").arg(totalChecked));

    for (int i = 0; i < txs.size(); i++) {
        const QJsonValue& val = txs[i];
        QJsonObject tx = val.toObject();

        QString txHash = tx["transaction_id"].toString();
        QString toAddr = tx["to"].toString().toLower();
        QString fromAddr = tx["from"].toString().toLower();
        QString contractAddr = tx["token_info"].toObject()["address"].toString().toLower();
        QString amountStr = tx["value"].toString();
        qint64 txTimestamp = tx["block_timestamp"].toVariant().toLongLong() / 1000; // 转为秒
        QDateTime txTime = QDateTime::fromSecsSinceEpoch(txTimestamp);

        emit logMessage(QString("[TronMonitor]   [%1/%2] 检查交易: %3")
            .arg(i + 1).arg(totalChecked).arg(txHash.left(16) + "..."));

        // 只处理 USDT TRC-20 转入平台地址的交易
        if (contractAddr != QString(USDT_CONTRACT).toLower()) {
            emit logMessage(QString("[TronMonitor]     ⏭️ 跳过: 非 USDT 合约 (%1)").arg(contractAddr.left(10)));
            skippedCount++;
            continue;
        }
        if (toAddr != m_receiveAddress.toLower()) {
            emit logMessage(QString("[TronMonitor]     ⏭️ 跳过: 收款地址不匹配 (to=%1)").arg(toAddr.left(10)));
            skippedCount++;
            continue;
        }

        // 跳过已处理的交易(通过 tx_hash 去重)
        if (m_processedTxHashes.contains(txHash)) {
            emit logMessage(QString("[TronMonitor]     ⏭️ 跳过: 已处理过的交易"));
            skippedCount++;
            continue;
        }

        // 将 SUN 转为 USDT(1 USDT = 1,000,000 SUN)
        double amountSun = amountStr.toDouble();
        double amountUsdt = amountSun / 1000000.0;

        // 四舍五入到 2 位小数,与订单金额匹配
        amountUsdt = qRound(amountUsdt * 100.0) / 100.0;

        emit logMessage(QString("[TronMonitor]     💰 金额: %1 USDT, 时间: %2, 来源: %3")
            .arg(amountUsdt, 0, 'f', 3)
            .arg(txTime.toString("yyyy-MM-dd hh:mm:ss"))
            .arg(fromAddr.left(10) + "..." + fromAddr.right(4)));

        // 查找匹配的待支付订单
        if (!m_mysql || !m_mysql->isConnected()) {
            emit logMessage("[TronMonitor]     ❌ MySQL 未连接,跳过订单匹配");
            skippedCount++;
            continue;
        }

        // 关键条件:
        // 1. status = 0(待支付)
        // 2. amount 精确匹配(带小数)
        // 3. create_time < txTimestamp(链上交易时间必须晚于订单创建时间)
        QString whereClause = QString("status = 0 AND amount = %1 AND payment_method = 'usdt' AND create_time < %2")
            .arg(amountUsdt, 0, 'f', 3)
            .arg(txTimestamp);
        emit logMessage(QString("[TronMonitor]     🔎 查询订单: %1").arg(whereClause));

        QVariantList orders = m_mysql->selectList("user_recharge", whereClause, "id ASC", 10);

        if (orders.isEmpty()) {
            emit logMessage(QString("[TronMonitor]     ⚠️ 未找到匹配的待支付订单"));
            // 标记为已处理,防止下次重复扫描
            m_processedTxHashes.insert(txHash);
            if (m_redis && m_redis->isConnected()) {
                m_redis->lpush("tron:processed_txs", txHash);
                m_redis->expire("tron:processed_txs", 86400); // 保留 24 小时
            }
            skippedCount++;
            continue;
        }

        emit logMessage(QString("[TronMonitor]     📋 找到 %1 个候选订单").arg(orders.size()));

        // 可能有多个订单金额相同,取最早创建的且 create_time < txTimestamp 的
        QVariantMap matchedOrder;
        for (const QVariant& ord : orders) {
            QVariantMap order = ord.toMap();
            int orderCreateTime = order["create_time"].toInt();
            QDateTime orderTime = QDateTime::fromSecsSinceEpoch(orderCreateTime);
            emit logMessage(QString("[TronMonitor]       订单 id=%1, 创建时间=%2, 金额=%3")
                .arg(order["id"].toInt())
                .arg(orderTime.toString("yyyy-MM-dd hh:mm:ss"))
                .arg(order["amount"].toDouble(), 0, 'f', 3));

            if (orderCreateTime < txTimestamp) {
                matchedOrder = order;
                emit logMessage(QString("[TronMonitor]       ✅ 匹配成功: 订单创建时间早于链上交易时间"));
                break; // 取第一个符合条件的(按 id ASC 排序,即最早创建的)
            }
            else {
                emit logMessage(QString("[TronMonitor]       ️ 跳过: 订单创建时间晚于链上交易时间"));
            }
        }

        if (matchedOrder.isEmpty()) {
            emit logMessage(QString("[TronMonitor]      所有候选订单的创建时间都晚于链上交易时间,跳过"));
            m_processedTxHashes.insert(txHash);
            if (m_redis && m_redis->isConnected()) {
                m_redis->lpush("tron:processed_txs", txHash);
                m_redis->expire("tron:processed_txs", 86400);
            }
            skippedCount++;
            continue;
        }

        int orderId = matchedOrder["id"].toInt();
        int userId = matchedOrder["user_id"].toInt();

        emit logMessage(QString("[TronMonitor]     🎯 最终匹配: order_id=%1, user_id=%2, amount=%3 USDT")
            .arg(orderId).arg(userId).arg(amountUsdt, 0, 'f', 3));

        // 确认订单
        confirmOrder(orderId, userId, amountUsdt, txHash, txTimestamp);

        // 标记为已处理
        m_processedTxHashes.insert(txHash);
        if (m_redis && m_redis->isConnected()) {
            m_redis->lpush("tron:processed_txs", txHash);
            m_redis->expire("tron:processed_txs", 86400); // 保留 24 小时
        }

        matchedCount++;
    }

    emit logMessage(QString("[TronMonitor]  本轮总结: 共检查 %1 笔, 匹配 %2 笔, 跳过 %3 笔")
        .arg(totalChecked).arg(matchedCount).arg(skippedCount));
}

bool TronMonitor::isTxProcessed(const QString& txHash)
{
    return m_processedTxHashes.contains(txHash);
}

void TronMonitor::confirmOrder(int orderId, int userId, double amount, const QString& txHash, qint64 txTimestamp)
{
    if (!m_mysql || !m_mysql->isConnected()) {
        emit logMessage("[TronMonitor] ❌ MySQL 未连接,无法确认订单");
        return;
    }

    emit logMessage(QString("[TronMonitor] 🔐 开始确认订单: order_id=%1, user_id=%2, amount=%3 USDT")
        .arg(orderId).arg(userId).arg(amount, 0, 'f', 3));

    // 1. 检查订单是否已被处理(防重复)
    QVariantMap order = m_mysql->selectOne("user_recharge", QString("id = %1").arg(orderId));
    if (order.isEmpty()) {
        emit logMessage(QString("[TronMonitor] ❌ 订单不存在: id=%1").arg(orderId));
        return;
    }
    if (order["status"].toInt() > 0) {
        emit logMessage(QString("[TronMonitor] ⚠️ 订单已确认,跳过: id=%1").arg(orderId));
        return;
    }

    emit logMessage(QString("[TronMonitor]   当前订单状态: status=%1, create_time=%2")
        .arg(order["status"].toInt())
        .arg(QDateTime::fromSecsSinceEpoch(order["create_time"].toInt()).toString("yyyy-MM-dd hh:mm:ss")));

    // 2. 更新订单状态为已支付,并保存 tx_hash
    QVariantMap updateData;
    updateData["status"] = 1;
    updateData["pay_time"] = (int)txTimestamp; // 使用链上交易时间
    updateData["tx_hash"] = txHash;

    bool updateSuccess = m_mysql->update("user_recharge", updateData, QString("id = %1").arg(orderId));
    if (!updateSuccess) {
        emit logMessage(QString("[TronMonitor]  更新订单失败: %1").arg(m_mysql->lastError()));
        return;
    }
    emit logMessage("[TronMonitor]   ✅ 订单状态更新成功");

    // 3. 给用户加余额
    QString sql = QString("UPDATE %1users SET balance = balance + %2 WHERE id = %3")
        .arg(m_mysql->tablePrefix())
        .arg(amount, 0, 'f', 3)
        .arg(userId);
    bool addBalanceSuccess = m_mysql->execute(sql);
    if (!addBalanceSuccess) {
        emit logMessage(QString("[TronMonitor] ❌ 增加余额失败: %1").arg(m_mysql->lastError()));
        return;
    }
    emit logMessage(QString("[TronMonitor]   ✅ 用户余额增加成功: user_id=%1, + %2 USDT").arg(userId).arg(amount, 0, 'f', 3));

    // 4. 查询用户最新余额用于日志
    QVariantMap userInfo = m_mysql->selectOne("users", QString("id = %1").arg(userId));
    double newBalance = userInfo["balance"].toDouble();

    emit logMessage(QString("[TronMonitor]  订单确认完成: order_id=%1, user_id=%2, amount=%3 USDT, tx=%4, 新余额=%5 USDT")
        .arg(orderId).arg(userId).arg(amount, 0, 'f', 3).arg(txHash.left(16) + "...").arg(newBalance, 0, 'f', 3));
}

头文件:

#ifndef TRONMONITOR_H
#define TRONMONITOR_H

#include <QObject>
#include <QTimer>
#include <QNetworkAccessManager>
#include <QSslConfiguration>
#include <QSslSocket>
#include <QSslError>
#include <QSet>
#include "MysqlClient.h"
#include "RedisClient.h"

class TronMonitor : public QObject
{
    Q_OBJECT
public:
    explicit TronMonitor(QObject* parent = nullptr);

    void setMysql(MysqlClient* mysql) { m_mysql = mysql; }
    void setRedis(RedisClient* redis) { m_redis = redis; }
    void setApiKey(const QString& key) { m_apiKey = key; }
    void setReceiveAddress(const QString& addr) { m_receiveAddress = addr; }
    void setPollInterval(int seconds) { m_pollInterval = seconds; }

    void start();
    void stop();

signals:
    void logMessage(const QString& msg);

private slots:
    void onPoll();
    void onReplyFinished(QNetworkReply* reply);

private:
    void processTransactions(const QJsonArray& txs);
    void confirmOrder(int orderId, int userId, double amount, const QString& txHash, qint64 txTimestamp);
    bool isTxProcessed(const QString& txHash);
    QNetworkReply* m_pendingReply = nullptr;  // 当前待处理的回复
    QNetworkAccessManager* m_net = nullptr;
    QTimer* m_timer = nullptr;
    MysqlClient* m_mysql = nullptr;
    RedisClient* m_redis = nullptr;

    QString m_apiKey;
    QString m_receiveAddress;
    QSet<QString> m_processedTxHashes; // 内存缓存已处理的 tx_hash
    int m_pollInterval = 30;           // 轮询间隔(秒)
    static constexpr const char* USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; // USDT TRC-20
};

#endif // TRONMONITOR_H