diff --git a/README.html b/README.html
new file mode 100644
index 0000000..a85ee1f
--- /dev/null
+++ b/README.html
@@ -0,0 +1,1523 @@
+
+
+
+
+ M2Pool Payment System v2
+
+
+
+
+
+
+
+
+
+
+
+
+
+ M2Pool Payment System v2
+
+
基于以太坊区块链的分布式支付系统
+
+
+
+
+
+
支持 充值 、提现 、支付 三大核心功能,实时监听链上交易,自动确认到账。
+
基于 Go 1.24 + Ethereum + RabbitMQ + MySQL 技术栈构建的企业级支付解决方案。
+
快速开始 • 项目架构 • 功能特性 • 性能测试 • 常见问题
+
+
+📋 目录
+
+
+项目简介
+M2Pool Payment System v2 是一个基于以太坊区块链的分布式支付解决方案 ,提供完整的数字货币充值、提现、支付功能。
+核心能力
+
+🔍 实时监听 :订阅链上事件,实时检测 USDT 转账
+⚡ 快速确认 :20 个区块确认,约 4-5 分钟到账
+🔒 安全可靠 :私钥加密存储,签名验证机制
+📊 高并发 :支持中等并发(50-200 TPS)
+🔄 自动重连 :WebSocket 断开自动重连
+📨 消息队列 :基于 RabbitMQ 的异步通信
+
+技术栈
+
+
+
+组件
+技术
+版本
+
+
+
+
+语言
+Go
+1.24+
+
+
+区块链
+Ethereum
+go-ethereum v1.16.4
+
+
+消息队列
+RabbitMQ
+amqp091-go v1.10.0
+
+
+数据库
+MySQL
+8.0+
+
+
+网络协议
+WebSocket + RPC
+-
+
+
+
+
+项目架构
+系统架构图
+┌─────────────────────────────────────────────────────────────┐
+│ 业务系统 │
+│ (Web/App/API Server) │
+└────────────┬────────────────────────────┬───────────────────┘
+ │ │
+ │ 请求 │ 响应
+ ↓ ↑
+┌─────────────────────────────────────────────────────────────┐
+│ RabbitMQ │
+│ ┌─────────┐ ┌──────────┐ ┌────────┐ │
+│ │ topup │ │ withdraw │ │ pay │ 请求队列 │
+│ └─────────┘ └──────────┘ └────────┘ │
+│ ┌─────────┐ ┌──────────┐ ┌────────┐ │
+│ │topup_ │ │withdraw_ │ │ pay_ │ 响应队列 │
+│ │ resp │ │ resp │ │ resp │ │
+│ └─────────┘ └──────────┘ └────────┘ │
+└────────────┬────────────────────────────┬───────────────────┘
+ │ │
+ ↓ ↑
+┌─────────────────────────────────────────────────────────────┐
+│ M2Pool Payment System v2 │
+│ │
+│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ RabbitMQ │ │ Blockchain │ │ Database │ │
+│ │ Consumer │─>│ Manager │─>│ (MySQL) │ │
+│ └──────────────┘ └──────┬───────┘ └──────────────┘ │
+│ │ │
+│ │ WebSocket + RPC │
+└────────────────────────────┼─────────────────────────────────┘
+ │
+ ↓
+┌─────────────────────────────────────────────────────────────┐
+│ 以太坊区块链网络 │
+│ │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ 新区块 │ │ USDT Transfer │ │
+│ │ (NewHead) │ │ 事件 │ │
+│ └──────────────┘ └──────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+
+核心模块
+1. Blockchain Manager (internal/blockchain/)
+
+blockchain.go :统一的区块链接口定义
+eth/eth.go :以太坊节点实现
+
+监听 USDT Transfer 事件(实时检测充值)
+监听新区块产生(触发交易确认)
+管理待确认交易池(UnConfirmTxs)
+执行 ERC20 转账(提现/支付)
+自动重连机制
+地址统一小写处理
+
+
+
+2. Message Queue (internal/queue/)
+
+rabbitmq.go :RabbitMQ 消息队列服务
+
+消费 3 个请求队列(充值/提现/支付)
+发布 3 个响应队列(交易确认结果)
+自动重连和错误重试
+消息持久化
+
+
+
+3. Database (internal/db/)
+
+db.go :MySQL 数据库连接池
+
+存储钱包私钥(加密)
+连接池管理
+事务支持
+
+
+
+4. Message (internal/msg/)
+
+msg.go :消息结构定义
+
+请求消息(TopupMsg_req, WithdrawMsg_req, PayMsg_req)
+响应消息(TopupMsg_resp, WithdrawMsg_resp, PayMsg_resp)
+配置结构(Config, RMQConfig, ETHConfig)
+
+
+
+5. Utils (internal/utils/)
+
+utils.go :工具函数
+
+数值转换(BigInt ↔ Float64)
+哈希计算
+加密解密
+
+
+
+6. Crypto (internal/crypto/)
+
+7. Server (internal/server.go)
+
+服务启动和管理
+消息路由和处理
+优雅关闭
+
+项目结构
+m2pool-payment-v2/
+├── cmd/ # 主程序入口
+│ └── main.go # 程序入口,解析命令行参数
+├── internal/ # 内部包(不对外暴露)
+│ ├── server.go # 服务启动和管理
+│ ├── blockchain/ # 区块链交互模块
+│ │ ├── blockchain.go # 统一的区块链接口定义
+│ │ ├── eth/ # 以太坊实现
+│ │ │ └── eth.go # USDT 监听、转账、确认
+│ │ └── tron/ # TRON 实现(待开发)
+│ ├── crypto/ # 加密工具
+│ │ └── crypto.go # SHA256、签名验证
+│ ├── db/ # 数据库
+│ │ ├── db.go # MySQL 连接池管理
+│ │ └── sqlite.go # SQLite 本地存储
+│ ├── msg/ # 消息定义
+│ │ └── msg.go # 请求/响应结构体定义
+│ ├── queue/ # 消息队列
+│ │ ├── rabbitmq.go # RabbitMQ 客户端封装
+│ │ └── README.md # RabbitMQ 使用文档
+│ ├── logger/ # 日志记录
+│ │ └── transaction_logger.go # 交易日志
+│ └── utils/ # 工具函数
+│ └── utils.go # 类型转换、格式化
+├── public/ # 公共资源
+│ ├── SQLite3.sql # SQLite 表结构
+│ └── migration.sql # 数据库迁移脚本
+├── test/ # 测试和示例
+│ ├── test.go # 测试程序(独立运行)
+│ └── config.json # 配置文件
+├── go.mod # Go 模块定义
+├── go.sum # 依赖版本锁定
+└── README.md # 项目文档(本文件)
+
+核心特性详解
+1. 双重监听机制 🎯
+系统同时监听两种链上事件:
+① USDT Transfer 事件监听
+
+e.WsClient.SubscribeFilterLogs(query, e.USDT.LogsChan)
+
+② 新区块头监听
+
+e.WsClient.SubscribeNewHead(e.Ctx, headers)
+
+2. 智能交易确认 ⚡
+事件驱动 + 区块驱动 :
+
+Transfer 事件到达时立即检查
+每个新区块产生时也检查
+确保交易在第 20 个区块后立即确认
+
+3. 地址统一规范 🔡
+所有以太坊地址统一转换为小写 :
+
+避免大小写不一致导致的匹配失败。
+4. 并发安全设计 🔒
+
+sync.Map 用于高并发地址监听
+sync.Mutex 保护共享数据结构
+Channel 缓冲区防止阻塞
+Goroutine panic 恢复机制
+
+5. 余额智能管理 💰
+自动归集钱包切换:
+用户钱包余额 < 转账金额
+ ↓
+自动使用归集钱包
+ ↓
+确保交易成功
+
+6. Gas 费用检查 ⛽
+转账前自动检查:
+
+USDT 余额是否足够
+ETH 余额是否足够支付 Gas
+预估 Gas 价格
+
+
+功能特性
+1. 充值功能 💰
+用户转账 → 实时检测 → 待确认通知 → 区块确认 → 最终通知
+
+特点:
+
+✅ 实时检测到账
+✅ 发送两次 通知:待确认 + 最终确认
+✅ 支持多币种(当前支持 USDT)
+✅ 自动地址监听管理
+
+消息流:
+
+业务系统发送充值请求 → RabbitMQ
+系统添加地址监听
+用户转账 → 立即通知(status=2 待确认)
+等待 20 个区块 → 最终通知(status=1 成功 / 0 失败)
+
+2. 提现功能 💸
+提现请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果
+
+特点:
+
+✅ 自动余额检查
+✅ 余额不足时使用归集钱包
+✅ 发送一次 通知:最终确认
+✅ Gas 费用检查
+
+消息流:
+
+业务系统发送提现请求 → RabbitMQ
+系统验证余额并发送交易
+等待 20 个区块确认
+返回结果(status=1 成功 / 0 失败)
+
+3. 支付功能 💳
+支付请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果
+
+特点:
+
+✅ 订单关联
+✅ 自动余额检查
+✅ 发送一次 通知:最终确认
+✅ 支持商户收款
+
+消息流:
+
+业务系统发送支付请求(含订单ID)→ RabbitMQ
+系统验证余额并发送交易
+等待 20 个区块确认
+返回结果(status=1 成功 / 0 失败)
+
+
+快速开始
+前置条件
+
+✅ Go 1.24 或更高版本
+✅ MySQL 8.0+
+✅ RabbitMQ 3.x+
+✅ 以太坊节点(支持 WebSocket 和 RPC)
+
+安装步骤
+
+git clone <repository-url>
+cd m2pool-payment-v2
+
+
+go mod download
+
+
+mysql -u root -p
+
+CREATE DATABASE payment CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+USE payment;
+
+
+CREATE TABLE `eth_balance` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `address` VARCHAR (42 ) NOT NULL UNIQUE ,
+ `private_key` VARCHAR (255 ) NOT NULL COMMENT '加密后的私钥' ,
+ `balance` DECIMAL (20 , 8 ) DEFAULT 0 ,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ,
+ INDEX `idx_address` (`address`)
+) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COMMENT= 'ETH钱包表' ;
+
+
+cd test
+cp config.json config.json.backup
+
+
+
+cd ..
+go build -o m2pool-payment cmd/main.go
+
+
+./m2pool-payment -key=your_secret_key
+
+
+cd test
+go run test.go
+
+配置文件示例
+创建 test/config.json:
+{
+ "rmq_config" : {
+ "sub_addr" : "amqp://m2pool:m2pool@localhost:5672" ,
+ "pay" : {
+ "queue" : "pay.auto.queue" ,
+ "exchange" : "pay.exchange" ,
+ "routing" : [ "pay.auto.routing.key" ]
+ } ,
+ "topup" : {
+ "queue" : "pay.recharge.queue" ,
+ "exchange" : "pay.exchange" ,
+ "routing" : [ "pay.recharge.routing.key" ]
+ } ,
+ "withdraw" : {
+ "queue" : "pay.withdraw.queue" ,
+ "exchange" : "pay.exchange" ,
+ "routing" : [ "pay.withdraw.routing.key" ]
+ } ,
+ "pay_resp" : {
+ "queue" : "pay.auto.return.queue" ,
+ "exchange" : "pay.exchange" ,
+ "routing" : [ "pay.auto.return.routing.key" ]
+ } ,
+ "topup_resp" : {
+ "queue" : "pay.recharge.return.queue" ,
+ "exchange" : "pay.exchange" ,
+ "routing" : [ "pay.recharge.return.routing.key" ]
+ } ,
+ "withdraw_resp" : {
+ "queue" : "pay.withdraw.return.queue" ,
+ "exchange" : "pay.exchange" ,
+ "routing" : [ "pay.withdraw.return.routing.key" ]
+ }
+ } ,
+ "eth_config" : {
+ "rpcUrl" : "http://localhost:8545" ,
+ "wsUrl" : "ws://localhost:8546" ,
+ "confirmHeight" : 20 ,
+ "dbConfig" : {
+ "user" : "root" ,
+ "password" : "your_password" ,
+ "host" : "127.0.0.1" ,
+ "port" : 3306 ,
+ "database" : "payment" ,
+ "maxOpenConns" : 20 ,
+ "maxIdleCoons" : 20 ,
+ "connMaxLife" : 120
+ }
+ }
+}
+
+
+配置说明
+配置项说明
+
+
+
+配置项
+说明
+默认值
+必填
+
+
+
+
+rmq_config.sub_addr
+RabbitMQ 连接地址
+-
+✅
+
+
+eth_config.rpcUrl
+以太坊 RPC 地址
+-
+✅
+
+
+eth_config.wsUrl
+以太坊 WebSocket 地址
+-
+✅
+
+
+eth_config.confirmHeight
+确认区块数
+20
+✅
+
+
+dbConfig.user
+数据库用户名
+root
+✅
+
+
+dbConfig.password
+数据库密码
+-
+✅
+
+
+dbConfig.database
+数据库名称
+payment
+✅
+
+
+
+
+使用示例
+1. 充值流程
+步骤 1:业务系统发送充值请求
+发送到 RabbitMQ 队列:pay.recharge.queue
+{
+ "chain" : "ETH" ,
+ "symbol" : "USDT" ,
+ "address" : "0x4e5b2e1dc63f6b91cb6cd759936495434c7e972f" ,
+ "timestamp" : 1758610297 ,
+ "sign" : "signature_hash"
+}
+
+步骤 2:用户转账
+用户向指定地址转账 USDT
+步骤 3:接收通知
+从 RabbitMQ 队列:pay.recharge.return.queue 接收两次 消息:
+第一次(待确认):
+{
+ "address" : "0x4e5b2e1dc63f6b91cb6cd759936495434c7e972f" ,
+ "status" : 2 ,
+ "chain" : "ETH" ,
+ "symbol" : "USDT" ,
+ "amount" : 100.5 ,
+ "tx_hash" : "0xabc..."
+}
+
+第二次(最终确认):
+{
+ "address" : "0x4e5b2e1dc63f6b91cb6cd759936495434c7e972f" ,
+ "status" : 1 ,
+ "chain" : "ETH" ,
+ "symbol" : "USDT" ,
+ "amount" : 100.5 ,
+ "tx_hash" : "0xabc..."
+}
+
+2. 提现流程
+步骤 1:业务系统发送提现请求
+发送到 RabbitMQ 队列:pay.withdraw.queue
+{
+ "queue_id" : "withdraw_123" ,
+ "from_address" : "0x1111..." ,
+ "to_address" : "0x2222..." ,
+ "amount" : 50.0 ,
+ "chain" : "ETH" ,
+ "symbol" : "USDT" ,
+ "timestamp" : 1758610297 ,
+ "sign" : "signature_hash"
+}
+
+步骤 2:系统处理
+
+验证签名
+检查余额
+发送链上交易
+等待确认
+
+步骤 3:接收通知
+从 RabbitMQ 队列:pay.withdraw.return.queue 接收一次 消息:
+{
+ "queue_id" : "withdraw_123" ,
+ "status" : 1 ,
+ "amount" : 50.0 ,
+ "chain" : "ETH" ,
+ "symbol" : "USDT" ,
+ "tx_hash" : "0xdef..."
+}
+
+3. 支付流程
+步骤 1:业务系统发送支付请求
+发送到 RabbitMQ 队列:pay.auto.queue
+{
+ "queue_id" : "pay_456" ,
+ "from_address" : "0x1111..." ,
+ "to_address" : "0x3333..." ,
+ "amount" : 200.0 ,
+ "chain" : "ETH" ,
+ "symbol" : "USDT" ,
+ "order_id" : "order_789" ,
+ "timestamp" : 1758610297 ,
+ "sign" : "signature_hash"
+}
+
+步骤 2:系统处理
+
+验证签名
+检查余额
+发送链上交易
+等待确认
+
+步骤 3:接收通知
+从 RabbitMQ 队列:pay.auto.return.queue 接收一次 消息:
+{
+ "queue_id" : "pay_456" ,
+ "status" : 1 ,
+ "amount" : 200.0 ,
+ "chain" : "ETH" ,
+ "symbol" : "USDT" ,
+ "order_id" : "order_789" ,
+ "tx_hash" : "0xghi..."
+}
+
+
+常见问题
+Q1: 为什么充值会收到两次通知?
+A: 这是设计特性!
+
+第一次 (status=2):检测到交易,提醒用户"正在确认"
+第二次 (status=1/0):交易确认,通知最终结果
+
+业务系统应该:
+
+status=2:显示进度,不增加余额
+status=1:增加余额
+
+Q2: 提现/支付为什么只有一次通知?
+A: 因为是系统主动发起的交易,用户已经知道在处理中,不需要额外的待确认通知。
+Q3: 如何处理交易失败?
+A: 系统会返回 status=0 的消息,业务系统应该:
+
+充值失败:不增加余额,提示用户联系客服
+提现失败:退回用户余额
+支付失败:恢复订单状态,退回余额
+
+Q4: 确认需要多长时间?
+A: 配置为 20 个区块确认,以太坊约 12 秒/块:
+
+理论时间:20 × 12 = 240 秒(4 分钟)
+实际时间:4-5 分钟(包括网络延迟)
+
+Q5: 如何保证私钥安全?
+A:
+
+私钥加密存储 :数据库中存储加密后的私钥
+临时解密 :仅在转账时临时解密,用完立即释放
+访问控制 :数据库限制访问权限
+建议方案 :
+
+使用 AES-256 加密私钥
+使用 HSM(硬件安全模块)
+使用云服务商的 KMS(密钥管理服务)
+使用环境变量传递解密密钥
+
+
+
+⚠️ 重要 :当前代码中的解密逻辑是占位代码 ,生产环境必须替换为真实的加密算法!
+Q6: 余额不足时如何处理?
+A: 系统会自动使用归集钱包 转账。归集钱包应该:
+
+保持足够的余额
+定期从各个钱包归集资金
+设置余额告警
+
+Q7: 支持哪些网络?
+A:
+
+✅ 以太坊主网(Mainnet)
+✅ 以太坊测试网(Goerli, Sepolia)
+✅ 私有链
+⚠️ 需要修改 USDT 合约地址
+
+Q8: Gas 费用谁承担?
+A:
+
+充值 :用户承担(用户自己发送交易)
+提现 :平台承担(系统发送交易)
+支付 :平台承担(系统发送交易)
+
+建议:提现/支付时从用户金额中扣除 Gas 费
+
+部署指南
+Docker 部署(推荐)
+
+FROM golang:1.24 -alpine AS builder
+
+WORKDIR /app
+COPY . .
+RUN go mod download
+RUN CGO_ENABLED=0 GOOS=linux go build -o m2pool-payment cmd/main.go
+
+FROM alpine:latest
+RUN apk --no-cache add ca-certificates
+WORKDIR /root/
+
+COPY --from=builder /app/m2pool-payment .
+COPY --from=builder /app/test/config.json .
+
+CMD ["./m2pool-payment" ]
+
+
+version: '3.8'
+
+services:
+ payment:
+ build: .
+ depends_on:
+ - mysql
+ - rabbitmq
+ environment:
+ - CONFIG_PATH=/root/config.json
+ volumes:
+ - ./config.json:/root/config.json
+ restart: unless-stopped
+
+ mysql:
+ image: mysql:8.0
+ environment:
+ MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
+ MYSQL_DATABASE: payment
+ ports:
+ - "3306:3306"
+ volumes:
+ - mysql_data:/var/lib/mysql
+
+ rabbitmq:
+ image: rabbitmq:3-management
+ environment:
+ RABBITMQ_DEFAULT_USER: m2pool
+ RABBITMQ_DEFAULT_PASS: m2pool
+ ports:
+ - "5672:5672"
+ - "15672:15672"
+ volumes:
+ - rabbitmq_data:/var/lib/rabbitmq
+
+volumes:
+ mysql_data:
+ rabbitmq_data:
+
+
+docker-compose up -d
+
+
+docker-compose logs -f payment
+
+
+docker-compose down
+
+系统服务部署
+
+sudo useradd -r -s /bin/false m2pool
+
+
+sudo nano /etc/systemd/system/m2pool-payment.service
+
+[Unit]
+Description =M2Pool Payment System
+After =network.target mysql.service rabbitmq-server.service
+
+[Service]
+Type =simple
+User =m2pool
+WorkingDirectory =/opt/m2pool-payment
+ExecStart =/opt/m2pool-payment/m2pool-payment
+Restart =on -failure
+RestartSec =10
+
+[Install]
+WantedBy =multi-user.target
+
+
+sudo systemctl daemon-reload
+sudo systemctl enable m2pool-payment
+sudo systemctl start m2pool-payment
+
+
+sudo systemctl status m2pool-payment
+
+
+sudo journalctl -u m2pool-payment -f
+
+
+性能测试
+压测环境配置
+
+
+
+配置项
+规格
+说明
+
+
+
+
+CPU
+4核 2.4GHz
+Intel/AMD x64
+
+
+内存
+8GB DDR4
+系统 + 缓存
+
+
+网络
+100Mbps
+公网带宽
+
+
+数据库
+MySQL 8.0
+本地部署
+
+
+消息队列
+RabbitMQ 3.x
+本地部署
+
+
+区块链节点
+Infura/Alchemy
+云端服务
+
+
+
+预估压测结果
+1. 充值功能压测
+
+
+
+并发用户数
+TPS
+响应时间
+成功率
+说明
+
+
+
+
+10
+50-80
+< 2秒
+99.9%
+轻量负载,性能优秀
+
+
+50
+200-300
+2-5秒
+99.5%
+中等负载,性能良好
+
+
+100
+300-500
+5-10秒
+99.0%
+高负载,性能稳定
+
+
+200
+400-600
+10-20秒
+98.5%
+极限负载,性能下降
+
+
+500+
+500-800
+> 20秒
+< 98%
+超负载,不建议
+
+
+
+特点:
+
+✅ 充值检测性能优秀(纯监听,无链上操作)
+✅ 内存占用低(仅地址监听)
+✅ 网络带宽消耗小
+
+2. 提现功能压测
+
+
+
+并发用户数
+TPS
+响应时间
+成功率
+说明
+
+
+
+
+5
+20-30
+3-8秒
+99.8%
+轻量负载,性能优秀
+
+
+20
+50-80
+8-15秒
+99.0%
+中等负载,性能良好
+
+
+50
+80-120
+15-30秒
+98.5%
+高负载,性能稳定
+
+
+100
+100-150
+30-60秒
+97.0%
+极限负载,性能下降
+
+
+200+
+120-200
+> 60秒
+< 95%
+超负载,不建议
+
+
+
+特点:
+
+⚠️ 受链上交易确认时间影响(4-5分钟)
+⚠️ 数据库查询和私钥解密开销
+⚠️ Gas费用波动影响成功率
+
+3. 支付功能压测
+
+
+
+并发用户数
+TPS
+响应时间
+成功率
+说明
+
+
+
+
+5
+20-30
+3-8秒
+99.8%
+轻量负载,性能优秀
+
+
+20
+50-80
+8-15秒
+99.0%
+中等负载,性能良好
+
+
+50
+80-120
+15-30秒
+98.5%
+高负载,性能稳定
+
+
+100
+100-150
+30-60秒
+97.0%
+极限负载,性能下降
+
+
+200+
+120-200
+> 60秒
+< 95%
+超负载,不建议
+
+
+
+特点:
+
+⚠️ 与提现功能性能相近
+⚠️ 订单ID处理增加少量开销
+⚠️ 商户地址验证开销
+
+系统资源占用
+内存使用情况
+
+
+
+并发数
+基础内存
+峰值内存
+说明
+
+
+
+
+10
+100MB
+150MB
+轻量运行
+
+
+50
+200MB
+350MB
+中等负载
+
+
+100
+300MB
+500MB
+高负载
+
+
+200
+500MB
+800MB
+极限负载
+
+
+
+CPU使用情况
+
+
+
+并发数
+基础CPU
+峰值CPU
+说明
+
+
+
+
+10
+5%
+15%
+轻量运行
+
+
+50
+15%
+35%
+中等负载
+
+
+100
+25%
+50%
+高负载
+
+
+200
+40%
+70%
+极限负载
+
+
+
+网络带宽使用
+
+
+
+并发数
+上行带宽
+下行带宽
+说明
+
+
+
+
+10
+1Mbps
+2Mbps
+轻量运行
+
+
+50
+3Mbps
+5Mbps
+中等负载
+
+
+100
+5Mbps
+8Mbps
+高负载
+
+
+200
+8Mbps
+12Mbps
+极限负载
+
+
+
+性能瓶颈分析
+1. 主要瓶颈
+
+
+
+瓶颈类型
+影响程度
+解决方案
+
+
+
+
+区块链确认时间
+🔴 高
+使用Layer2或侧链
+
+
+数据库查询
+🟡 中
+添加索引,使用缓存
+
+
+私钥解密
+🟡 中
+优化加密算法
+
+
+网络延迟
+🟡 中
+使用CDN,就近部署
+
+
+Gas费用波动
+🟡 中
+动态Gas价格调整
+
+
+
+2. 优化建议
+短期优化(1-2周):
+
+✅ 添加数据库索引
+✅ 优化SQL查询语句
+✅ 增加Channel缓冲区大小
+✅ 使用连接池复用
+
+中期优化(1-2月):
+
+🔄 实现Redis缓存
+🔄 优化私钥加密算法
+🔄 添加负载均衡
+🔄 实现读写分离
+
+长期优化(3-6月):
+
+🚀 支持Layer2网络
+🚀 实现微服务架构
+🚀 添加水平扩展
+🚀 使用分布式数据库
+
+压测工具推荐
+1. 消息队列压测
+
+docker run --rm -it \
+ -e RABBITMQ_HOST=localhost \
+ -e RABBITMQ_PORT=5672 \
+ -e RABBITMQ_USER=m2pool \
+ -e RABBITMQ_PASS=m2pool \
+ rabbitmq-perf-test:latest \
+ --rate 100 --time 60 --queue pay.withdraw.queue
+
+2. 数据库压测
+
+sysbench mysql \
+ --mysql-host=localhost \
+ --mysql-port=3306 \
+ --mysql-user=root \
+ --mysql-password=password \
+ --mysql-db=payment \
+ --tables=1 \
+ --table-size=10000 \
+ --threads=10 \
+ --time=60 \
+ run
+
+3. 系统监控
+
+htop
+iotop
+nethogs
+
+生产环境建议
+1. 推荐配置
+
+
+
+环境
+CPU
+内存
+并发数
+说明
+
+
+
+
+开发环境
+2核
+4GB
+< 10
+功能测试
+
+
+测试环境
+4核
+8GB
+< 50
+压力测试
+
+
+生产环境
+8核
+16GB
+< 100
+稳定运行
+
+
+高可用环境
+16核
+32GB
+< 200
+负载均衡
+
+
+
+2. 监控指标
+
+type Metrics struct {
+ TotalTransactions int64
+ PendingTransactions int
+ FailedTransactions int64
+ ChannelUsage int
+ LastBlockHeight uint64
+ MemoryUsage int64
+ CPUUsage float64
+}
+
+3. 告警阈值
+
+
+
+指标
+警告阈值
+严重阈值
+说明
+
+
+
+
+待确认交易数
+> 100
+> 500
+交易积压
+
+
+Channel使用率
+> 80%
+> 95%
+消息积压
+
+
+交易失败率
+> 5%
+> 10%
+系统异常
+
+
+内存使用率
+> 80%
+> 95%
+内存不足
+
+
+CPU使用率
+> 70%
+> 90%
+CPU过载
+
+
+
+
+重要修复说明
+🔧 已修复的问题
+1. QueueId 重复问题
+问题 :两笔不同的交易出现相同的 QueueId
+原因 :数据库表主键设计错误,使用 (from_addr, to_addr) 作为主键
+修复 :
+
+修改数据库表结构,将主键改为 queueId
+创建数据库迁移脚本 public/migration.sql
+修复 SQL 插入语句的参数数量不匹配问题
+
+2. 重复发送响应问题
+问题 :提现和支付会发送两次响应
+原因 :转账失败时,先发送失败响应,然后仍然进入链上确认流程
+修复 :
+
+在转账失败时添加 return 语句
+确保转账失败时不进入链上确认流程
+只有转账成功才会进入链上监听和确认
+
+📊 修复后的消息发送次数
+
+
+
+场景
+消息处理阶段
+链上确认阶段
+总发送次数
+
+
+
+
+转账失败
+发送失败响应
+不进入(已return)
+1次
+
+
+转账成功
+不发送响应
+发送成功响应
+1次
+
+
+
+
+贡献指南
+欢迎贡献代码!请遵循以下步骤:
+提交流程
+
+Fork 本项目
+创建功能分支 (git checkout -b feature/AmazingFeature)
+提交更改 (git commit -m 'feat: Add some AmazingFeature')
+推送到分支 (git push origin feature/AmazingFeature)
+开启 Pull Request
+
+代码规范
+
+✅ 使用 gofmt 格式化代码
+✅ 遵循 Go 命名规范
+✅ 添加必要的注释
+✅ 所有地址统一小写处理
+✅ 使用状态码常量(不要硬编码数字)
+✅ 添加错误处理和日志
+✅ 更新相关文档
+
+Commit 规范
+
+feat: 添加 BTC 网络支持
+
+
+fix: 修复充值消息重复发送问题
+
+
+docs: 更新 API 文档
+
+
+perf: 优化交易确认性能
+
+
+refactor: 重构数据库连接池
+
+
+test : 添加单元测试
+
+
+许可证
+MIT License
+Copyright (c) 2025 M2Pool Team
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
⭐ 如果这个项目对你有帮助,请给一个 Star!⭐
+
Made with ❤️ by M2Pool Team
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 61b53c2..ca4123a 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
基于 Go 1.24 + Ethereum + RabbitMQ + MySQL 技术栈构建的企业级支付解决方案。
-[快速开始](#快速开始) • [功能特性](#功能特性) • [架构设计](#架构设计) • [配置说明](#配置说明)
+[快速开始](#快速开始) • [项目架构](#项目架构) • [功能特性](#功能特性) • [性能测试](#性能测试) • [常见问题](#常见问题)
@@ -23,18 +23,15 @@
## 📋 目录
- [项目简介](#项目简介)
+- [项目架构](#项目架构)
- [功能特性](#功能特性)
-- [架构设计](#架构设计)
- [快速开始](#快速开始)
- [配置说明](#配置说明)
- [使用示例](#使用示例)
-- [API 文档](#api-文档)
-- [开发指南](#开发指南)
-- [部署指南](#部署指南)
-- [性能指标](#性能指标)
- [常见问题](#常见问题)
+- [部署指南](#部署指南)
+- [性能测试](#性能测试)
- [贡献指南](#贡献指南)
-- [许可证](#许可证)
---
@@ -47,7 +44,7 @@ M2Pool Payment System v2 是一个基于以太坊区块链的**分布式支付
- 🔍 **实时监听**:订阅链上事件,实时检测 USDT 转账
- ⚡ **快速确认**:20 个区块确认,约 4-5 分钟到账
- 🔒 **安全可靠**:私钥加密存储,签名验证机制
-- 📊 **高并发**:支持少量并发(50-200 TPS)
+- 📊 **高并发**:支持中等并发(50-200 TPS)
- 🔄 **自动重连**:WebSocket 断开自动重连
- 📨 **消息队列**:基于 RabbitMQ 的异步通信
@@ -63,69 +60,7 @@ M2Pool Payment System v2 是一个基于以太坊区块链的**分布式支付
---
-## 功能特性
-
-### 1. 充值功能 💰
-
-```
-用户转账 → 实时检测 → 待确认通知 → 区块确认 → 最终通知
-```
-
-**特点:**
-- ✅ 实时检测到账
-- ✅ 发送**两次**通知:待确认 + 最终确认
-- ✅ 支持多币种(当前支持 USDT)
-- ✅ 自动地址监听管理
-
-**消息流:**
-1. 业务系统发送充值请求 → RabbitMQ
-2. 系统添加地址监听
-3. 用户转账 → 立即通知(status=2 待确认)
-4. 等待 20 个区块 → 最终通知(status=1 成功 / 0 失败)
-
----
-
-### 2. 提现功能 💸
-
-```
-提现请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果
-```
-
-**特点:**
-- ✅ 自动余额检查
-- ✅ 余额不足时使用归集钱包
-- ✅ 发送**一次**通知:最终确认
-- ✅ Gas 费用检查
-
-**消息流:**
-1. 业务系统发送提现请求 → RabbitMQ
-2. 系统验证余额并发送交易
-3. 等待 20 个区块确认
-4. 返回结果(status=1 成功 / 0 失败)
-
----
-
-### 3. 支付功能 💳
-
-```
-支付请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果
-```
-
-**特点:**
-- ✅ 订单关联
-- ✅ 自动余额检查
-- ✅ 发送**一次**通知:最终确认
-- ✅ 支持商户收款
-
-**消息流:**
-1. 业务系统发送支付请求(含订单ID)→ RabbitMQ
-2. 系统验证余额并发送交易
-3. 等待 20 个区块确认
-4. 返回结果(status=1 成功 / 0 失败)
-
----
-
-## 架构设计
+## 项目架构
### 系统架构图
@@ -218,6 +153,161 @@ M2Pool Payment System v2 是一个基于以太坊区块链的**分布式支付
- 消息路由和处理
- 优雅关闭
+### 项目结构
+
+```
+m2pool-payment-v2/
+├── cmd/ # 主程序入口
+│ └── main.go # 程序入口,解析命令行参数
+├── internal/ # 内部包(不对外暴露)
+│ ├── server.go # 服务启动和管理
+│ ├── blockchain/ # 区块链交互模块
+│ │ ├── blockchain.go # 统一的区块链接口定义
+│ │ ├── eth/ # 以太坊实现
+│ │ │ └── eth.go # USDT 监听、转账、确认
+│ │ └── tron/ # TRON 实现(待开发)
+│ ├── crypto/ # 加密工具
+│ │ └── crypto.go # SHA256、签名验证
+│ ├── db/ # 数据库
+│ │ ├── db.go # MySQL 连接池管理
+│ │ └── sqlite.go # SQLite 本地存储
+│ ├── msg/ # 消息定义
+│ │ └── msg.go # 请求/响应结构体定义
+│ ├── queue/ # 消息队列
+│ │ ├── rabbitmq.go # RabbitMQ 客户端封装
+│ │ └── README.md # RabbitMQ 使用文档
+│ ├── logger/ # 日志记录
+│ │ └── transaction_logger.go # 交易日志
+│ └── utils/ # 工具函数
+│ └── utils.go # 类型转换、格式化
+├── public/ # 公共资源
+│ ├── SQLite3.sql # SQLite 表结构
+│ └── migration.sql # 数据库迁移脚本
+├── test/ # 测试和示例
+│ ├── test.go # 测试程序(独立运行)
+│ └── config.json # 配置文件
+├── go.mod # Go 模块定义
+├── go.sum # 依赖版本锁定
+└── README.md # 项目文档(本文件)
+```
+
+### 核心特性详解
+
+#### 1. 双重监听机制 🎯
+
+系统同时监听两种链上事件:
+
+**① USDT Transfer 事件监听**
+```go
+// 检测 USDT 转账,用于充值检测和交易确认触发
+e.WsClient.SubscribeFilterLogs(query, e.USDT.LogsChan)
+```
+
+**② 新区块头监听**
+```go
+// 每个新区块触发交易确认检查,确保及时确认
+e.WsClient.SubscribeNewHead(e.Ctx, headers)
+```
+
+#### 2. 智能交易确认 ⚡
+
+**事件驱动 + 区块驱动**:
+- Transfer 事件到达时立即检查
+- 每个新区块产生时也检查
+- **确保交易在第 20 个区块后立即确认**
+
+#### 3. 地址统一规范 🔡
+
+所有以太坊地址**统一转换为小写**:
+- 存储时转换
+- 比较时转换
+- 查询时转换
+
+避免大小写不一致导致的匹配失败。
+
+#### 4. 并发安全设计 🔒
+
+- `sync.Map` 用于高并发地址监听
+- `sync.Mutex` 保护共享数据结构
+- Channel 缓冲区防止阻塞
+- Goroutine panic 恢复机制
+
+#### 5. 余额智能管理 💰
+
+**自动归集钱包切换:**
+```
+用户钱包余额 < 转账金额
+ ↓
+自动使用归集钱包
+ ↓
+确保交易成功
+```
+
+#### 6. Gas 费用检查 ⛽
+
+转账前自动检查:
+- USDT 余额是否足够
+- ETH 余额是否足够支付 Gas
+- 预估 Gas 价格
+
+---
+
+## 功能特性
+
+### 1. 充值功能 💰
+
+```
+用户转账 → 实时检测 → 待确认通知 → 区块确认 → 最终通知
+```
+
+**特点:**
+- ✅ 实时检测到账
+- ✅ 发送**两次**通知:待确认 + 最终确认
+- ✅ 支持多币种(当前支持 USDT)
+- ✅ 自动地址监听管理
+
+**消息流:**
+1. 业务系统发送充值请求 → RabbitMQ
+2. 系统添加地址监听
+3. 用户转账 → 立即通知(status=2 待确认)
+4. 等待 20 个区块 → 最终通知(status=1 成功 / 0 失败)
+
+### 2. 提现功能 💸
+
+```
+提现请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果
+```
+
+**特点:**
+- ✅ 自动余额检查
+- ✅ 余额不足时使用归集钱包
+- ✅ 发送**一次**通知:最终确认
+- ✅ Gas 费用检查
+
+**消息流:**
+1. 业务系统发送提现请求 → RabbitMQ
+2. 系统验证余额并发送交易
+3. 等待 20 个区块确认
+4. 返回结果(status=1 成功 / 0 失败)
+
+### 3. 支付功能 💳
+
+```
+支付请求 → 验证余额 → 发送交易 → 等待确认 → 返回结果
+```
+
+**特点:**
+- ✅ 订单关联
+- ✅ 自动余额检查
+- ✅ 发送**一次**通知:最终确认
+- ✅ 支持商户收款
+
+**消息流:**
+1. 业务系统发送支付请求(含订单ID)→ RabbitMQ
+2. 系统验证余额并发送交易
+3. 等待 20 个区块确认
+4. 返回结果(status=1 成功 / 0 失败)
+
---
## 快速开始
@@ -284,37 +374,7 @@ go run test.go
```json
{
"rmq_config": {
- "sub_addr": "amqp://m2pool:m2pool@localhost:5672"
- // ... 其他配置见下文
- },
- "eth_config": {
- "rpcUrl": "http://localhost:8545",
- "wsUrl": "ws://localhost:8546",
- "confirmHeight": 20,
- "dbConfig": {
- "user": "root",
- "password": "your_password",
- "host": "127.0.0.1",
- "port": 3306,
- "database": "payment",
- "maxOpenConns": 20,
- "maxIdleCoons": 20,
- "connMaxLife": 120
- }
- }
-}
-```
-
----
-
-## 配置说明
-
-### 配置文件结构 (config.json)
-
-```json
-{
- "rmq_config": {
- "sub_addr": "amqp://username:password@localhost:5672",
+ "sub_addr": "amqp://m2pool:m2pool@localhost:5672",
"pay": {
"queue": "pay.auto.queue",
"exchange": "pay.exchange",
@@ -364,6 +424,10 @@ go run test.go
}
```
+---
+
+## 配置说明
+
### 配置项说明
| 配置项 | 说明 | 默认值 | 必填 |
@@ -428,8 +492,6 @@ go run test.go
}
```
----
-
### 2. 提现流程
**步骤 1:业务系统发送提现请求**
@@ -471,8 +533,6 @@ go run test.go
}
```
----
-
### 3. 支付流程
**步骤 1:业务系统发送支付请求**
@@ -518,271 +578,72 @@ go run test.go
---
-## API 文档
+## 常见问题
-### 状态码说明
+### Q1: 为什么充值会收到两次通知?
-| 状态码 | 常量名 | 说明 | 适用场景 |
-|--------|--------|------|---------|
-| `0` | STATUS_FAILED | 交易失败 | 交易被回退或执行失败 |
-| `1` | STATUS_SUCCESS | 交易成功 | 交易成功并已确认 |
-| `2` | STATUS_PENDING | 待确认 | 交易已检测到,等待区块确认 |
-| `3` | STATUS_VERIFY_FAILED | 验证失败 | 签名验证失败 |
+**A:** 这是设计特性!
+- **第一次**(status=2):检测到交易,提醒用户"正在确认"
+- **第二次**(status=1/0):交易确认,通知最终结果
-### 消息结构
+业务系统应该:
+- status=2:显示进度,**不增加余额**
+- status=1:增加余额
-#### 充值请求 (TopupMsg_req)
+### Q2: 提现/支付为什么只有一次通知?
-| 字段 | 类型 | 说明 | 必填 |
-|------|------|------|------|
-| chain | string | 链名称 (ETH) | ✅ |
-| symbol | string | 币种 (USDT) | ✅ |
-| address | string | 充值地址 | ✅ |
-| timestamp | uint64 | 时间戳 | ✅ |
-| sign | string | 签名 | ✅ |
+**A:** 因为是系统主动发起的交易,用户已经知道在处理中,不需要额外的待确认通知。
-#### 充值响应 (TopupMsg_resp)
+### Q3: 如何处理交易失败?
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| address | string | 充值地址 |
-| status | int | 状态码 (0/1/2/3) |
-| chain | string | 链名称 |
-| symbol | string | 币种 |
-| amount | float64 | 金额 |
-| tx_hash | string | 交易哈希 |
+**A:** 系统会返回 status=0 的消息,业务系统应该:
+- 充值失败:不增加余额,提示用户联系客服
+- 提现失败:退回用户余额
+- 支付失败:恢复订单状态,退回余额
-#### 提现请求 (WithdrawMsg_req)
+### Q4: 确认需要多长时间?
-| 字段 | 类型 | 说明 | 必填 |
-|------|------|------|------|
-| queue_id | string | 队列ID | ✅ |
-| from_address | string | 转出地址 | ✅ |
-| to_address | string | 转入地址 | ✅ |
-| amount | float64 | 金额 | ✅ |
-| chain | string | 链名称 | ✅ |
-| symbol | string | 币种 | ✅ |
-| timestamp | uint64 | 时间戳 | ✅ |
-| sign | string | 签名 | ✅ |
+**A:** 配置为 20 个区块确认,以太坊约 12 秒/块:
+- 理论时间:20 × 12 = 240 秒(4 分钟)
+- 实际时间:4-5 分钟(包括网络延迟)
-#### 提现响应 (WithdrawMsg_resp)
+### Q5: 如何保证私钥安全?
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| queue_id | string | 队列ID |
-| status | int | 状态码 |
-| amount | float64 | 金额 |
-| chain | string | 链名称 |
-| symbol | string | 币种 |
-| tx_hash | string | 交易哈希 |
+**A:**
+1. **私钥加密存储**:数据库中存储加密后的私钥
+2. **临时解密**:仅在转账时临时解密,用完立即释放
+3. **访问控制**:数据库限制访问权限
+4. **建议方案**:
+ - 使用 AES-256 加密私钥
+ - 使用 HSM(硬件安全模块)
+ - 使用云服务商的 KMS(密钥管理服务)
+ - 使用环境变量传递解密密钥
-#### 支付请求 (PayMsg_req)
+⚠️ **重要**:当前代码中的解密逻辑是**占位代码**,生产环境必须替换为真实的加密算法!
-| 字段 | 类型 | 说明 | 必填 |
-|------|------|------|------|
-| queue_id | string | 队列ID | ✅ |
-| from_address | string | 付款地址 | ✅ |
-| to_address | string | 收款地址(商户) | ✅ |
-| amount | float64 | 金额 | ✅ |
-| chain | string | 链名称 | ✅ |
-| symbol | string | 币种 | ✅ |
-| order_id | string | 订单ID | ✅ |
-| timestamp | uint64 | 时间戳 | ✅ |
-| sign | string | 签名 | ✅ |
+### Q6: 余额不足时如何处理?
-#### 支付响应 (PayMsg_resp)
+**A:** 系统会自动使用**归集钱包**转账。归集钱包应该:
+- 保持足够的余额
+- 定期从各个钱包归集资金
+- 设置余额告警
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| queue_id | string | 队列ID |
-| status | int | 状态码 |
-| amount | float64 | 金额 |
-| chain | string | 链名称 |
-| symbol | string | 币种 |
-| order_id | string | 订单ID |
-| tx_hash | string | 交易哈希 |
+### Q7: 支持哪些网络?
----
+**A:**
+- ✅ 以太坊主网(Mainnet)
+- ✅ 以太坊测试网(Goerli, Sepolia)
+- ✅ 私有链
+- ⚠️ 需要修改 USDT 合约地址
-## 开发指南
+### Q8: Gas 费用谁承担?
-### 项目结构
+**A:**
+- **充值**:用户承担(用户自己发送交易)
+- **提现**:平台承担(系统发送交易)
+- **支付**:平台承担(系统发送交易)
-```
-m2pool-payment-v2/
-├── cmd/ # 主程序入口
-│ └── main.go # 程序入口,解析命令行参数
-├── internal/ # 内部包(不对外暴露)
-│ ├── server.go # 服务启动和管理
-│ ├── blockchain/ # 区块链交互模块
-│ │ ├── blockchain.go # 统一的区块链接口定义
-│ │ ├── eth/ # 以太坊实现
-│ │ │ └── eth.go # USDT 监听、转账、确认
-│ │ └── tron/ # TRON 实现(待开发)
-│ ├── crypto/ # 加密工具
-│ │ └── crypto.go # SHA256、签名验证
-│ ├── db/ # 数据库
-│ │ └── db.go # MySQL 连接池管理
-│ ├── msg/ # 消息定义
-│ │ └── msg.go # 请求/响应结构体定义
-│ ├── queue/ # 消息队列
-│ │ ├── rabbitmq.go # RabbitMQ 客户端封装
-│ │ └── README.md # RabbitMQ 使用文档
-│ └── utils/ # 工具函数
-│ └── utils.go # 类型转换、格式化
-├── test/ # 测试和示例
-│ ├── test.go # 测试程序(独立运行)
-│ └── config.json # 配置文件
-├── go.mod # Go 模块定义
-├── go.sum # 依赖版本锁定
-└── README.md # 项目文档(本文件)
-```
-
-### 代码统计
-
-| 模块 | 文件 | 代码行数 | 说明 |
-|------|------|---------|------|
-| **eth** | eth.go | ~700 | 以太坊核心逻辑 |
-| **queue** | rabbitmq.go | ~350 | RabbitMQ 封装 |
-| **server** | server.go | ~300 | 服务管理 |
-| **msg** | msg.go | ~130 | 消息定义 |
-| **blockchain** | blockchain.go | ~70 | 接口定义 |
-| **其他** | - | ~200 | 工具、数据库等 |
-| **总计** | - | **~1750** | - |
-
-### 开发环境设置
-
-#### 方式一:Docker 快速启动(推荐)
-
-```bash
-# 1. 启动 MySQL
-docker run -d \
- --name m2pool-mysql \
- -p 3306:3306 \
- -e MYSQL_ROOT_PASSWORD=Lzx2021@! \
- -e MYSQL_DATABASE=payment \
- -v mysql_data:/var/lib/mysql \
- mysql:8.0
-
-# 2. 启动 RabbitMQ
-docker run -d \
- --name m2pool-rabbitmq \
- -p 5672:5672 \
- -p 15672:15672 \
- -e RABBITMQ_DEFAULT_USER=m2pool \
- -e RABBITMQ_DEFAULT_PASS=m2pool \
- -v rabbitmq_data:/var/lib/rabbitmq \
- rabbitmq:3-management
-
-# 3. 访问 RabbitMQ 管理界面
-# http://localhost:15672
-# 用户名: m2pool
-# 密码: m2pool
-```
-
-#### 方式二:本地安装
-
-```bash
-# Ubuntu/Debian
-sudo apt update
-sudo apt install golang-1.24 mysql-server rabbitmq-server
-
-# CentOS/RHEL
-sudo yum install golang mysql-server rabbitmq-server
-
-# macOS
-brew install go mysql rabbitmq
-```
-
-#### 以太坊节点选择
-
-**选项 1:使用公共节点服务(推荐)**
-```bash
-# Infura(免费层每天 100,000 请求)
-RPC: https://mainnet.infura.io/v3/YOUR_API_KEY
-WS: wss://mainnet.infura.io/ws/v3/YOUR_API_KEY
-
-# Alchemy(免费层每秒 25 请求)
-RPC: https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
-WS: wss://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
-```
-
-**选项 2:自建节点**
-```bash
-# 使用 Geth
-geth --http --http.addr 0.0.0.0 --http.port 8545 \
- --ws --ws.addr 0.0.0.0 --ws.port 8546 \
- --http.api eth,net,web3 --ws.api eth,net,web3
-
-# 或使用 Erigon(更轻量)
-erigon --http --ws
-```
-
-**选项 3:测试网络**
-```bash
-# Goerli 测试网(免费)
-RPC: https://goerli.infura.io/v3/YOUR_API_KEY
-WS: wss://goerli.infura.io/ws/v3/YOUR_API_KEY
-
-# Sepolia 测试网(推荐)
-RPC: https://sepolia.infura.io/v3/YOUR_API_KEY
-WS: wss://sepolia.infura.io/ws/v3/YOUR_API_KEY
-```
-
-### 数据库表结构
-
-```sql
--- 钱包余额表(必须)
-CREATE TABLE `eth_balance` (
- `id` INT AUTO_INCREMENT PRIMARY KEY,
- `address` VARCHAR(42) NOT NULL UNIQUE COMMENT '钱包地址(小写)',
- `private_key` VARCHAR(255) NOT NULL COMMENT '加密后的私钥',
- `balance` DECIMAL(20, 8) DEFAULT 0 COMMENT 'USDT余额',
- `eth_balance` DECIMAL(20, 18) DEFAULT 0 COMMENT 'ETH余额(用于Gas)',
- `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- INDEX `idx_address` (`address`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ETH钱包表';
-
--- 插入归集钱包示例
-INSERT INTO `eth_balance` (`address`, `private_key`, `balance`, `eth_balance`)
-VALUES ('归集钱包', 'encrypted_private_key_here', 10000.00, 1.0);
-
--- 交易记录表(可选,用于审计)
-CREATE TABLE `transactions` (
- `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
- `tx_hash` VARCHAR(66) NOT NULL UNIQUE COMMENT '交易哈希',
- `from_address` VARCHAR(42) NOT NULL COMMENT '发送地址',
- `to_address` VARCHAR(42) NOT NULL COMMENT '接收地址',
- `amount` DECIMAL(20, 8) NOT NULL COMMENT '金额',
- `symbol` VARCHAR(10) NOT NULL COMMENT '币种',
- `chain` VARCHAR(10) NOT NULL COMMENT '链名称',
- `tx_type` TINYINT NOT NULL COMMENT '0=充值,1=提现,2=支付',
- `status` TINYINT NOT NULL COMMENT '0=失败,1=成功,2=待确认',
- `block_height` BIGINT COMMENT '区块高度',
- `queue_id` VARCHAR(50) COMMENT '队列ID',
- `order_id` VARCHAR(50) COMMENT '订单ID',
- `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- `confirmed_at` TIMESTAMP NULL COMMENT '确认时间',
- INDEX `idx_tx_hash` (`tx_hash`),
- INDEX `idx_from` (`from_address`),
- INDEX `idx_to` (`to_address`),
- INDEX `idx_status` (`status`),
- INDEX `idx_order_id` (`order_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易记录表';
-```
-
-### 重要说明
-
-⚠️ **私钥安全**:
-- 数据库中存储的是**加密后的私钥**
-- 需要实现真实的加密/解密逻辑
-- 当前代码中的解密逻辑是占位代码,需要替换为实际的加密算法(如 AES-256)
-
-⚠️ **归集钱包**:
-- 必须在数据库中配置归集钱包
-- 地址字段填写 `"归集钱包"` 字符串
-- 保持足够的 USDT 和 ETH 余额
+建议:提现/支付时从用户金额中扣除 Gas 费
---
@@ -904,567 +765,238 @@ sudo journalctl -u m2pool-payment -f
---
-## 性能指标
+## 性能测试
-### 处理能力
+### 压测环境配置
-| 指标 | 数值 | 说明 |
-|------|------|------|
-| **充值检测 TPS** | 500-1000 | 每秒处理交易数 |
-| **提现/支付 TPS** | 200-500 | 包含数据库查询 |
-| **消息发送 TPS** | 5000-10000 | RabbitMQ 发送速率 |
-| **并发地址监听** | 10000+ | 同时监听的地址数量 |
+| 配置项 | 规格 | 说明 |
+|--------|------|------|
+| **CPU** | 4核 2.4GHz | Intel/AMD x64 |
+| **内存** | 8GB DDR4 | 系统 + 缓存 |
+| **网络** | 100Mbps | 公网带宽 |
+| **数据库** | MySQL 8.0 | 本地部署 |
+| **消息队列** | RabbitMQ 3.x | 本地部署 |
+| **区块链节点** | Infura/Alchemy | 云端服务 |
-### 响应时间
+### 预估压测结果
-| 操作 | 响应时间 | 说明 |
-|------|---------|------|
-| **充值待确认通知** | < 3 秒 | 检测到交易后 |
-| **充值最终确认** | 4-5 分钟 | 20 个区块 |
-| **提现执行** | < 5 秒 | 发送交易 |
-| **提现最终确认** | 4-5 分钟 | 20 个区块 |
+#### 1. 充值功能压测
-### 资源占用(4核8G环境)
+| 并发用户数 | TPS | 响应时间 | 成功率 | 说明 |
+|-----------|-----|---------|--------|------|
+| **10** | 50-80 | < 2秒 | 99.9% | 轻量负载,性能优秀 |
+| **50** | 200-300 | 2-5秒 | 99.5% | 中等负载,性能良好 |
+| **100** | 300-500 | 5-10秒 | 99.0% | 高负载,性能稳定 |
+| **200** | 400-600 | 10-20秒 | 98.5% | 极限负载,性能下降 |
+| **500+** | 500-800 | > 20秒 | < 98% | 超负载,不建议 |
-| 资源 | 使用量 | 峰值 |
-|------|--------|------|
-| **CPU** | 5-15% | 30% |
-| **内存** | 100-300 MB | 500 MB |
-| **网络带宽** | 1-5 MB/s | 10 MB/s |
-| **数据库连接** | 5-10 | 20 |
+**特点:**
+- ✅ 充值检测性能优秀(纯监听,无链上操作)
+- ✅ 内存占用低(仅地址监听)
+- ✅ 网络带宽消耗小
----
+#### 2. 提现功能压测
-## 常见问题
+| 并发用户数 | TPS | 响应时间 | 成功率 | 说明 |
+|-----------|-----|---------|--------|------|
+| **5** | 20-30 | 3-8秒 | 99.8% | 轻量负载,性能优秀 |
+| **20** | 50-80 | 8-15秒 | 99.0% | 中等负载,性能良好 |
+| **50** | 80-120 | 15-30秒 | 98.5% | 高负载,性能稳定 |
+| **100** | 100-150 | 30-60秒 | 97.0% | 极限负载,性能下降 |
+| **200+** | 120-200 | > 60秒 | < 95% | 超负载,不建议 |
-### Q1: 为什么充值会收到两次通知?
+**特点:**
+- ⚠️ 受链上交易确认时间影响(4-5分钟)
+- ⚠️ 数据库查询和私钥解密开销
+- ⚠️ Gas费用波动影响成功率
-**A:** 这是设计特性!
-- **第一次**(status=2):检测到交易,提醒用户"正在确认"
-- **第二次**(status=1/0):交易确认,通知最终结果
+#### 3. 支付功能压测
-业务系统应该:
-- status=2:显示进度,**不增加余额**
-- status=1:增加余额
+| 并发用户数 | TPS | 响应时间 | 成功率 | 说明 |
+|-----------|-----|---------|--------|------|
+| **5** | 20-30 | 3-8秒 | 99.8% | 轻量负载,性能优秀 |
+| **20** | 50-80 | 8-15秒 | 99.0% | 中等负载,性能良好 |
+| **50** | 80-120 | 15-30秒 | 98.5% | 高负载,性能稳定 |
+| **100** | 100-150 | 30-60秒 | 97.0% | 极限负载,性能下降 |
+| **200+** | 120-200 | > 60秒 | < 95% | 超负载,不建议 |
-### Q2: 提现/支付为什么只有一次通知?
+**特点:**
+- ⚠️ 与提现功能性能相近
+- ⚠️ 订单ID处理增加少量开销
+- ⚠️ 商户地址验证开销
-**A:** 因为是系统主动发起的交易,用户已经知道在处理中,不需要额外的待确认通知。
+### 系统资源占用
-### Q3: 如何处理交易失败?
+#### 内存使用情况
-**A:** 系统会返回 status=0 的消息,业务系统应该:
-- 充值失败:不增加余额,提示用户联系客服
-- 提现失败:退回用户余额
-- 支付失败:恢复订单状态,退回余额
+| 并发数 | 基础内存 | 峰值内存 | 说明 |
+|--------|----------|----------|------|
+| **10** | 100MB | 150MB | 轻量运行 |
+| **50** | 200MB | 350MB | 中等负载 |
+| **100** | 300MB | 500MB | 高负载 |
+| **200** | 500MB | 800MB | 极限负载 |
-### Q4: 确认需要多长时间?
+#### CPU使用情况
-**A:** 配置为 20 个区块确认,以太坊约 12 秒/块:
-- 理论时间:20 × 12 = 240 秒(4 分钟)
-- 实际时间:4-5 分钟(包括网络延迟)
+| 并发数 | 基础CPU | 峰值CPU | 说明 |
+|--------|---------|---------|------|
+| **10** | 5% | 15% | 轻量运行 |
+| **50** | 15% | 35% | 中等负载 |
+| **100** | 25% | 50% | 高负载 |
+| **200** | 40% | 70% | 极限负载 |
-### Q5: 如何保证私钥安全?
+#### 网络带宽使用
-**A:**
-1. **私钥加密存储**:数据库中存储加密后的私钥
-2. **临时解密**:仅在转账时临时解密,用完立即释放
-3. **访问控制**:数据库限制访问权限
-4. **建议方案**:
- - 使用 AES-256 加密私钥
- - 使用 HSM(硬件安全模块)
- - 使用云服务商的 KMS(密钥管理服务)
- - 使用环境变量传递解密密钥
+| 并发数 | 上行带宽 | 下行带宽 | 说明 |
+|--------|----------|----------|------|
+| **10** | 1Mbps | 2Mbps | 轻量运行 |
+| **50** | 3Mbps | 5Mbps | 中等负载 |
+| **100** | 5Mbps | 8Mbps | 高负载 |
+| **200** | 8Mbps | 12Mbps | 极限负载 |
-⚠️ **重要**:当前代码中的解密逻辑(`eth.go` 第 467 行)是**占位代码**,生产环境必须替换为真实的加密算法!
+### 性能瓶颈分析
-```go
-// ❌ 当前代码(占位)
-privateKey := encryptedKey + address + ""
+#### 1. 主要瓶颈
-// ✅ 应该改为(示例)
-privateKey := AES256Decrypt(encryptedKey, decodeKey)
+| 瓶颈类型 | 影响程度 | 解决方案 |
+|----------|----------|----------|
+| **区块链确认时间** | 🔴 高 | 使用Layer2或侧链 |
+| **数据库查询** | 🟡 中 | 添加索引,使用缓存 |
+| **私钥解密** | 🟡 中 | 优化加密算法 |
+| **网络延迟** | 🟡 中 | 使用CDN,就近部署 |
+| **Gas费用波动** | 🟡 中 | 动态Gas价格调整 |
+
+#### 2. 优化建议
+
+**短期优化(1-2周):**
+- ✅ 添加数据库索引
+- ✅ 优化SQL查询语句
+- ✅ 增加Channel缓冲区大小
+- ✅ 使用连接池复用
+
+**中期优化(1-2月):**
+- 🔄 实现Redis缓存
+- 🔄 优化私钥加密算法
+- 🔄 添加负载均衡
+- 🔄 实现读写分离
+
+**长期优化(3-6月):**
+- 🚀 支持Layer2网络
+- 🚀 实现微服务架构
+- 🚀 添加水平扩展
+- 🚀 使用分布式数据库
+
+### 压测工具推荐
+
+#### 1. 消息队列压测
+
+```bash
+# 使用 RabbitMQ 压测工具
+docker run --rm -it \
+ -e RABBITMQ_HOST=localhost \
+ -e RABBITMQ_PORT=5672 \
+ -e RABBITMQ_USER=m2pool \
+ -e RABBITMQ_PASS=m2pool \
+ rabbitmq-perf-test:latest \
+ --rate 100 --time 60 --queue pay.withdraw.queue
```
-### Q6: 余额不足时如何处理?
+#### 2. 数据库压测
-**A:** 系统会自动使用**归集钱包**转账。归集钱包应该:
-- 保持足够的余额
-- 定期从各个钱包归集资金
-- 设置余额告警
-
-### Q7: 支持哪些网络?
-
-**A:**
-- ✅ 以太坊主网(Mainnet)
-- ✅ 以太坊测试网(Goerli, Sepolia)
-- ✅ 私有链
-- ⚠️ 需要修改 USDT 合约地址
-
-### Q8: Gas 费用谁承担?
-
-**A:**
-- **充值**:用户承担(用户自己发送交易)
-- **提现**:平台承担(系统发送交易)
-- **支付**:平台承担(系统发送交易)
-
-建议:提现/支付时从用户金额中扣除 Gas 费
-
-### Q9: 如何监控系统状态?
-
-**A:** 建议监控:
-- 待确认交易数量:`len(UnConfirmTxs)`
-- Channel 使用率:`len(chainEventCh)/cap(chainEventCh)`
-- RabbitMQ 连接状态
-- WebSocket 连接状态
-- 数据库连接池状态
-
-### Q10: 如何处理重复消息?
-
-**A:** RabbitMQ 可能重复投递消息,业务系统应该:
-1. 使用 `tx_hash` 作为唯一标识
-2. 实现幂等性处理
-3. 数据库添加唯一索引
-
-### Q11: 支持的 USDT 合约地址是什么?
-
-**A:** 当前配置的合约地址:
-- **以太坊主网**:`0xdAC17F958D2ee523a2206206994597C13D831ec7`
-- **测试网**:需要部署测试 ERC20 合约
-- **私有链**:需要部署自己的 ERC20 合约
-
-修改合约地址位置:`internal/blockchain/eth/eth.go` 第 99 行
-
-### Q12: 新区块监听的作用是什么?
-
-**A:** 新区块监听确保交易及时确认:
-- **问题**:如果只依赖 USDT Transfer 事件,在长时间无 USDT 转账时,待确认交易无法被确认
-- **解决**:监听新区块产生,每个新区块都检查待确认交易
-- **效果**:交易在达到第 20 个区块后,下一个区块就会被确认
-
-### Q13: Channel 缓冲区设置多大?
-
-**A:** 当前设置为 1000:
-```go
-chainEventCh := make(chan any, 1000)
+```bash
+# 使用 sysbench 压测 MySQL
+sysbench mysql \
+ --mysql-host=localhost \
+ --mysql-port=3306 \
+ --mysql-user=root \
+ --mysql-password=password \
+ --mysql-db=payment \
+ --tables=1 \
+ --table-size=10000 \
+ --threads=10 \
+ --time=60 \
+ run
```
-- **轻量负载**(<50 TPS):100 足够
-- **中等负载**(50-200 TPS):500-1000 推荐
-- **高负载**(>200 TPS):2000+ 或优化架构
+#### 3. 系统监控
-监控 Channel 使用率,避免满载丢消息。
+```bash
+# 监控系统资源
+htop # CPU和内存监控
+iotop # 磁盘IO监控
+nethogs # 网络带宽监控
+```
----
+### 生产环境建议
-## 安全建议
+#### 1. 推荐配置
-### 🔒 安全检查清单
+| 环境 | CPU | 内存 | 并发数 | 说明 |
+|------|-----|------|--------|------|
+| **开发环境** | 2核 | 4GB | < 10 | 功能测试 |
+| **测试环境** | 4核 | 8GB | < 50 | 压力测试 |
+| **生产环境** | 8核 | 16GB | < 100 | 稳定运行 |
+| **高可用环境** | 16核 | 32GB | < 200 | 负载均衡 |
-- [ ] 私钥加密存储
-- [ ] 使用 HTTPS/WSS 连接
-- [ ] 签名验证所有请求
-- [ ] 限制 API 访问频率
-- [ ] 定期备份数据库
-- [ ] 监控异常交易
-- [ ] 设置余额告警
-- [ ] 使用防火墙限制访问
-- [ ] 定期更新依赖包
-- [ ] 日志脱敏处理
-
-### ⚠️ 重要提示
-
-1. **私钥管理**
- - ❌ 不要在代码中硬编码私钥
- - ❌ 不要在日志中打印私钥
- - ✅ 使用环境变量或密钥管理服务
-
-2. **网络安全**
- - ✅ 使用 VPN 或专线连接区块链节点
- - ✅ RabbitMQ 启用 TLS
- - ✅ MySQL 限制远程访问
-
-3. **资金安全**
- - ✅ 设置单笔交易限额
- - ✅ 异常交易人工审核
- - ✅ 多签钱包(建议)
- - ✅ 冷热钱包分离
-
----
-
-## 监控告警
-
-### 推荐监控指标
+#### 2. 监控指标
```go
-// 添加监控指标
+// 关键监控指标
type Metrics struct {
- TotalTransactions int64 // 总交易数
- PendingTransactions int // 待确认交易数
- FailedTransactions int64 // 失败交易数
- ChannelUsage int // Channel使用率
- LastBlockHeight uint64 // 最新区块高度
-}
-
-// 定期上报
-go func() {
- ticker := time.NewTicker(1 * time.Minute)
- for range ticker.C {
- log.Printf("📊 待确认交易: %d", len(e.UnConfirmTxs))
- log.Printf("📊 监听地址数: %d", countAddresses())
- log.Printf("📊 Channel使用率: %d%%", len(ch)*100/cap(ch))
- }
-}()
-```
-
-### 告警规则建议
-
-| 指标 | 阈值 | 告警级别 |
-|------|------|---------|
-| 待确认交易数 | > 100 | ⚠️ 警告 |
-| 待确认交易数 | > 500 | 🔴 严重 |
-| Channel 使用率 | > 80% | ⚠️ 警告 |
-| Channel 使用率 | > 95% | 🔴 严重 |
-| 交易失败率 | > 5% | ⚠️ 警告 |
-| WebSocket 断线 | 重连 > 3次/小时 | ⚠️ 警告 |
-
----
-
-## 故障排查
-
-### 问题:充值检测不到
-
-**可能原因:**
-1. 地址未加入监听列表
-2. WebSocket 连接断开
-3. 合约地址配置错误
-4. 网络 ID 不匹配
-
-**排查步骤:**
-```bash
-# 检查日志
-grep "新增钱包监听消息" logs/payment.log
-
-# 检查订阅状态
-grep "订阅成功" logs/payment.log
-
-# 测试节点连接
-curl -X POST -H "Content-Type: application/json" \
- --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
- http://localhost:8545
-```
-
-### 问题:提现/支付失败
-
-**可能原因:**
-1. 余额不足
-2. Gas 费不足
-3. 私钥错误
-4. nonce 冲突
-
-**排查步骤:**
-```bash
-# 检查错误日志
-grep "转账失败" logs/payment.log
-
-# 检查余额
-grep "余额" logs/payment.log
-
-# 检查私钥
-grep "查询私钥" logs/payment.log
-```
-
-### 问题:消息未返回
-
-**可能原因:**
-1. RabbitMQ 连接断开
-2. Channel 阻塞
-3. 交易未确认
-
-**排查步骤:**
-```bash
-# 检查 RabbitMQ 连接
-rabbitmqctl list_connections
-
-# 检查队列状态
-rabbitmqctl list_queues
-
-# 检查待确认交易
-# 添加 HTTP 接口查询 UnConfirmTxs
-```
-
----
-
-## 注意事项
-
-### ⚠️ 生产环境部署前必须修改
-
-#### 1. 私钥加密实现(重要!)
-
-**位置**:`internal/blockchain/eth/eth.go` 第 467 行
-
-**当前代码**(占位):
-```go
-privateKey := encryptedKey + address + ""
-```
-
-**必须改为**(示例):
-```go
-// 使用 AES-256-GCM 加密
-import "crypto/aes"
-import "crypto/cipher"
-
-func (e *ETHNode) decodePrivatekey(address string) string {
- // 从数据库查询加密密钥
- encryptedKey := queryFromDB(address)
-
- // 使用 AES 解密
- privateKey := AESDecrypt(encryptedKey, e.decodeKey)
-
- return privateKey
+ TotalTransactions int64 // 总交易数
+ PendingTransactions int // 待确认交易数
+ FailedTransactions int64 // 失败交易数
+ ChannelUsage int // Channel使用率
+ LastBlockHeight uint64 // 最新区块高度
+ MemoryUsage int64 // 内存使用量
+ CPUUsage float64 // CPU使用率
}
```
-#### 2. USDT 合约地址验证
+#### 3. 告警阈值
-确认你的以太坊网络与合约地址匹配:
-
-| 网络 | Chain ID | USDT 合约地址 |
-|------|----------|--------------|
-| 主网 | 1 | `0xdAC17F958D2ee523a2206206994597C13D831ec7` ✅ |
-| Goerli | 5 | 需要部署测试合约 |
-| Sepolia | 11155111 | 需要部署测试合约 |
-| 私有链 | 自定义 | 需要部署自己的合约 |
-
-**修改位置**:`internal/blockchain/eth/eth.go` 第 99 行
-
-#### 3. 消息签名密钥
-
-**当前密钥**:`9f3c7a12`(测试用)
-
-**生产环境**:
-```bash
-# 生成强密钥
-openssl rand -hex 32
-
-# 启动时传入
-./m2pool-payment -key=your_production_secret_key
-```
-
-#### 4. 数据库密码安全
-
-- ❌ 不要在代码中硬编码密码
-- ✅ 使用环境变量
-- ✅ 使用配置管理工具(如 Vault)
+| 指标 | 警告阈值 | 严重阈值 | 说明 |
+|------|----------|----------|------|
+| **待确认交易数** | > 100 | > 500 | 交易积压 |
+| **Channel使用率** | > 80% | > 95% | 消息积压 |
+| **交易失败率** | > 5% | > 10% | 系统异常 |
+| **内存使用率** | > 80% | > 95% | 内存不足 |
+| **CPU使用率** | > 70% | > 90% | CPU过载 |
---
-## 贡献指南
+## 重要修复说明
-欢迎贡献代码!请遵循以下步骤:
+### 🔧 已修复的问题
-1. Fork 本项目
-2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
-3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
-4. 推送到分支 (`git push origin feature/AmazingFeature`)
-5. 开启 Pull Request
+#### 1. QueueId 重复问题
-### 代码规范
+**问题**:两笔不同的交易出现相同的 QueueId
-- ✅ 使用 `gofmt` 格式化代码
-- ✅ 遵循 Go 命名规范
-- ✅ 添加必要的注释
-- ✅ 所有地址统一小写处理
-- ✅ 使用状态码常量(不要硬编码数字)
-- ✅ 添加错误处理和日志
-- ✅ 更新相关文档
+**原因**:数据库表主键设计错误,使用 `(from_addr, to_addr)` 作为主键
-### 提交规范
+**修复**:
+- 修改数据库表结构,将主键改为 `queueId`
+- 创建数据库迁移脚本 `public/migration.sql`
+- 修复 SQL 插入语句的参数数量不匹配问题
-```bash
-# 功能
-feat: 添加 BTC 网络支持
+#### 2. 重复发送响应问题
-# 修复
-fix: 修复充值消息重复发送问题
+**问题**:提现和支付会发送两次响应
-# 文档
-docs: 更新 API 文档
+**原因**:转账失败时,先发送失败响应,然后仍然进入链上确认流程
-# 性能
-perf: 优化交易确认性能
+**修复**:
+- 在转账失败时添加 `return` 语句
+- 确保转账失败时不进入链上确认流程
+- 只有转账成功才会进入链上监听和确认
-# 重构
-refactor: 重构数据库连接池
-```
+### 📊 修复后的消息发送次数
----
-
-## 命令行参数
-
-```bash
-# 启动程序
-./m2pool-payment -key=your_secret_key
-
-# 参数说明
--key string
- 通信密钥,用于消息签名验证 (默认: "m2pool")
-```
-
-### 签名验证算法
-
-```go
-// 生成签名
-hash := SHA256(hex(timestamp) + secret_key)
-sign := hex.EncodeToString(hash)
-
-// 示例
-timestamp = 1758610297
-secret_key = "9f3c7a12"
-hash = SHA256("696a2d6929" + "9f3c7a12")
-sign = "219b3b3935f3d56db7eacd32aae84fa06df95806373d6fc4ed6e9b35ffb17f2d"
-```
-
----
-
-## 运行日志示例
-
-### 启动日志
-```
-========================================
-🚀 M2Pool Payment System Starting...
-========================================
-✅ 配置加载成功: RPC=http://10.168.3.236:18545, WS=ws://10.168.3.236:18546
-✅ 区块链服务初始化完成
-✅ RabbitMQ服务初始化完成: amqp://m2pool:m2pool@localhost:5672
-✅ RabbitMQ 监听启动完成
-========================================
-🎉 所有服务启动完成!
-========================================
-🔍 ETH 开始监听 USDT Transfer 事件...
-🔍 开始监听新区块...
-✅ 订阅成功
-✅ 新区块订阅成功
-```
-
-### 充值日志
-```
-📥 [RMQ] 收到充值请求: Chain=ETH, Symbol=USDT, Address=0x123...
-📨 [链上] 充值待确认: Address=0x123..., Amount=100.50, TxHash=0xabc...
-📤 [RMQ] 发送充值响应: Address=0x123..., Status=2, TxHash=0xabc...
-✅ [链上] 充值确认: Address=0x123..., Amount=100.50, TxHash=0xabc..., Status=1
-📤 [RMQ] 发送充值响应: Address=0x123..., Status=1, TxHash=0xabc...
-```
-
-### 提现日志
-```
-📥 [RMQ] 收到提现请求: QueueId=w123, From=0x111..., To=0x222..., Amount=50.00 USDT
-✅ [链上] 提现确认: QueueId=w123, Amount=50.00, TxHash=0xdef..., Status=1
-📤 [RMQ] 发送提现响应: QueueId=w123, Status=1, TxHash=0xdef...
-```
-
----
-
-## 路线图
-
-### ✅ v2.0 (已完成)
-- [x] 以太坊 USDT 支持
-- [x] 充值/提现/支付功能
-- [x] RabbitMQ 集成
-- [x] 双重监听机制
-- [x] 自动交易确认
-- [x] 地址统一规范
-- [x] Gas 费用检查
-- [x] Panic 恢复机制
-
-### 🚧 v2.1 (开发中)
-- [ ] 支持更多 ERC20 代币(USDC, DAI)
-- [ ] 交易记录持久化
-- [ ] HTTP API 接口
-- [ ] 私钥真实加密实现
-- [ ] 性能优化(读写锁、缓存)
-
-### 📋 v2.2 (计划中)
-- [ ] 支持 TRON 网络(TRC20-USDT)
-- [ ] 支持 BTC 网络
-- [ ] 多签钱包支持
-- [ ] 管理后台界面
-- [ ] 实时监控面板
-- [ ] 告警系统
-
-### 🔮 v3.0 (规划中)
-- [ ] 微服务架构拆分
-- [ ] 水平扩展支持
-- [ ] 分布式事务
-- [ ] 高可用集群部署
-- [ ] Kubernetes 支持
-
----
-
-## 核心特性详解
-
-### 1. 双重监听机制 🎯
-
-系统同时监听两种链上事件:
-
-**① USDT Transfer 事件监听**
-```go
-// 检测 USDT 转账,用于充值检测和交易确认触发
-e.WsClient.SubscribeFilterLogs(query, e.USDT.LogsChan)
-```
-
-**② 新区块头监听**
-```go
-// 每个新区块触发交易确认检查,确保及时确认
-e.WsClient.SubscribeNewHead(e.Ctx, headers)
-```
-
-### 2. 智能交易确认 ⚡
-
-**事件驱动 + 区块驱动**:
-- Transfer 事件到达时立即检查
-- 每个新区块产生时也检查
-- **确保交易在第 20 个区块后立即确认**
-
-### 3. 地址统一规范 🔡
-
-所有以太坊地址**统一转换为小写**:
-- 存储时转换
-- 比较时转换
-- 查询时转换
-
-避免大小写不一致导致的匹配失败。
-
-### 4. 并发安全设计 🔒
-
-- `sync.Map` 用于高并发地址监听
-- `sync.Mutex` 保护共享数据结构
-- Channel 缓冲区防止阻塞
-- Goroutine panic 恢复机制
-
-### 5. 余额智能管理 💰
-
-**自动归集钱包切换:**
-```
-用户钱包余额 < 转账金额
- ↓
-自动使用归集钱包
- ↓
-确保交易成功
-```
-
-### 6. Gas 费用检查 ⛽
-
-转账前自动检查:
-- USDT 余额是否足够
-- ETH 余额是否足够支付 Gas
-- 预估 Gas 价格
-
----
-
-## 相关文档
-
-- [RabbitMQ 使用说明](internal/queue/README.md)
+| 场景 | 消息处理阶段 | 链上确认阶段 | 总发送次数 |
+|------|-------------|-------------|-----------|
+| **转账失败** | 发送失败响应 | 不进入(已return) | **1次** |
+| **转账成功** | 不发送响应 | 发送成功响应 | **1次** |
---
@@ -1514,16 +1046,6 @@ test: 添加单元测试
---
-## 技术支持
-
-如有问题,欢迎通过以下方式联系:
-
-- 📧 Email: support@example.com
-- 💬 Issues: [GitHub Issues](https://github.com/your-repo/issues)
-- 📖 文档: [项目 Wiki](https://github.com/your-repo/wiki)
-
----
-
## 许可证
MIT License
@@ -1556,6 +1078,4 @@ SOFTWARE.
Made with ❤️ by M2Pool Team
-
-
-
+
\ No newline at end of file
diff --git a/bin/config.json b/bin/config.json
index c9b55ce..2b8b565 100644
--- a/bin/config.json
+++ b/bin/config.json
@@ -1,4 +1,7 @@
{
+ "sqlite3": {
+ "msg_path": "./msg.db"
+ },
"rmq_config": {
"sub_addr": "amqp://m2pool:m2pool@localhost:5672",
"pay": {
@@ -22,6 +25,13 @@
"pay.withdraw.routing.key"
]
},
+ "remove": {
+ "queue": "pay.remove.queue",
+ "exchange": "pay.exchange",
+ "routing": [
+ "pay.remove.routing.key"
+ ]
+ },
"pay_resp": {
"queue": "pay.auto.return.queue",
"exchange": "pay.exchange",
@@ -42,6 +52,13 @@
"routing": [
"pay.withdraw.return.routing.key"
]
+ },
+ "remove_resp": {
+ "queue": "pay.remove.return.queue",
+ "exchange": "pay.exchange",
+ "routing": [
+ "pay.remove.return.routing.key"
+ ]
}
},
"eth_config": {
diff --git a/go.mod b/go.mod
index 95ef2b6..dd39cdc 100644
--- a/go.mod
+++ b/go.mod
@@ -11,8 +11,17 @@ require (
require (
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/ncruces/go-strftime v0.1.9 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
google.golang.org/protobuf v1.36.6 // indirect
+ modernc.org/libc v1.66.10 // indirect
+ modernc.org/mathutil v1.7.1 // indirect
+ modernc.org/memory v1.11.0 // indirect
)
require (
@@ -36,6 +45,7 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
golang.org/x/crypto v0.36.0 // indirect
- golang.org/x/sync v0.12.0 // indirect
+ golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.36.0 // indirect
+ modernc.org/sqlite v1.39.1
)
diff --git a/go.sum b/go.sum
index ceafd51..8dac093 100644
--- a/go.sum
+++ b/go.sum
@@ -44,6 +44,8 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum/c-kzg-4844/v2 v2.1.3 h1:DQ21UU0VSsuGy8+pcMJHDS0CV1bKmJmxsJYK8l3MiLU=
@@ -75,6 +77,10 @@ github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXi
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
@@ -115,6 +121,8 @@ github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxd
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
@@ -141,6 +149,8 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
@@ -169,12 +179,15 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
-golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
-golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
+golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
+golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
@@ -183,6 +196,8 @@ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
+golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
@@ -191,3 +206,29 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
+modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
+modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
+modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
+modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
+modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
+modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
+modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
+modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/internal/blockchain/blockchain.go b/internal/blockchain/blockchain.go
index 99f6f52..7dbe6ff 100644
--- a/internal/blockchain/blockchain.go
+++ b/internal/blockchain/blockchain.go
@@ -6,8 +6,8 @@ import (
)
type IChainServer interface {
- AddAddress(address string, msg any)
- RemoveAddress(address string)
+ AddAddress(address string, msg any) error
+ RemoveAddress(address string) error
Listen(symbol string, ch chan any)
Transfer(symbol string, msg any) error
Stop()
@@ -30,21 +30,23 @@ func (b *BlockChainServer) RegisterChain(name string, chain IChainServer) {
b.chains[name] = chain
}
-func (b *BlockChainServer) AddAddress(chain, address string, msg any) {
+func (b *BlockChainServer) AddAddress(chain, address string, msg any) error {
if srv, ok := b.chains[chain]; ok {
srv.AddAddress(address, msg)
fmt.Printf("✅ 添加监听地址: chain=%s, address=%s\n", chain, address)
+ return nil
} else {
- fmt.Printf("⚠️ 链未注册: %s\n", chain)
+ return fmt.Errorf("⚠️ 链未注册: %s\n", chain)
}
}
-func (b *BlockChainServer) RemoveAddress(chain, address string) {
+func (b *BlockChainServer) RemoveAddress(chain, address string) error {
if srv, ok := b.chains[chain]; ok {
srv.RemoveAddress(address)
fmt.Printf("🗑️ 移除监听地址: chain=%s, address=%s\n", chain, address)
+ return nil
} else {
- fmt.Printf("⚠️ 链未注册: %s\n", chain)
+ return fmt.Errorf("⚠️ 链未注册: %s\n", chain)
}
}
diff --git a/internal/blockchain/eth/batch_transfer.go b/internal/blockchain/eth/batch_transfer.go
new file mode 100644
index 0000000..f82d5e1
--- /dev/null
+++ b/internal/blockchain/eth/batch_transfer.go
@@ -0,0 +1,213 @@
+package eth
+
+import (
+ "crypto/ecdsa"
+ "fmt"
+ "log"
+ "m2pool-payment/internal/utils"
+ "math/big"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+)
+
+// BatchTransferItem 批量转账单项
+type BatchTransferItem struct {
+ ToAddress string // 接收地址
+ Amount float64 // 转账金额
+}
+
+// BatchTransferResult 批量转账结果
+type BatchTransferResult struct {
+ TxHash string // 交易哈希
+ Success bool // 是否成功
+ TotalAmount float64 // 总转账金额
+ Count int // 转账笔数
+}
+
+// usdt_batch_transfer 批量转账ERC20-USDT
+// from: 发送地址
+// items: 批量转账列表
+// returns: 交易哈希和错误信息
+func (e *ETHNode) USDTBatchTransfer(from string, items []BatchTransferItem) (*BatchTransferResult, error) {
+ if len(items) == 0 {
+ return nil, fmt.Errorf("批量转账列表不能为空")
+ }
+
+ // 统一转换为小写
+ from = strings.ToLower(from)
+
+ // 计算总金额
+ var totalAmount float64
+ for _, item := range items {
+ if item.Amount <= 0 {
+ return nil, fmt.Errorf("转账金额必须大于0")
+ }
+ totalAmount += item.Amount
+ }
+
+ // 1. 校验钱包USDT余额
+ balance, err := e.getUSDTBalance(from)
+ log.Printf("🔄 批量转账 - 检测钱包=%s,余额=%.2f USDT", from, balance)
+ if err != nil {
+ return nil, fmt.Errorf("获取余额失败: %w", err)
+ }
+
+ if balance < totalAmount {
+ return nil, fmt.Errorf("余额不足: 余额=%.2f USDT < 需要=%.2f USDT", balance, totalAmount)
+ }
+
+ // 2. 通过from地址前往数据库查找出对应加密后的私钥,并解密真实的私钥
+ originalKey := e.decodePrivatekey(from)
+ if originalKey == "" {
+ return nil, fmt.Errorf("无法获取私钥")
+ }
+
+ privateKey, err := crypto.HexToECDSA(originalKey)
+ if err != nil {
+ return nil, fmt.Errorf("解析私钥失败: %w", err)
+ }
+
+ // 3. 获得nonce
+ nonce, err := e.RpcClient.PendingNonceAt(e.Ctx, common.HexToAddress(from))
+ if err != nil {
+ return nil, fmt.Errorf("获取nonce失败: %w", err)
+ }
+
+ // 4. 构造批量转账数据
+ // 使用 transfer(address[], uint256[]) 或多次transfer调用
+ // 这里使用多次transfer调用的方式,因为标准ERC20没有批量转账方法
+
+ // 方法1: 构造多次transfer调用(适合少量转账)
+ if len(items) <= 3 {
+ return e.batchTransferMultipleCalls(from, privateKey, nonce, items)
+ }
+
+ // 方法2: 使用合约批量转账(需要部署代理合约,这里简化处理)
+ // 注意:这里实现的是多次transaction方式
+ return e.batchTransferSeparateTransactions(from, privateKey, nonce, items)
+}
+
+// batchTransferMultipleCalls 使用一个交易多次调用transfer(需要gas优化)
+func (e *ETHNode) batchTransferMultipleCalls(from string, privateKey *ecdsa.PrivateKey, nonce uint64, items []BatchTransferItem) (*BatchTransferResult, error) {
+ // 注意:标准ERC20不支持批量transfer,这里需要自定义合约
+ // 或者使用多次独立交易
+ log.Printf("⚠️ 标准ERC20不支持批量transfer,改用多次独立交易")
+
+ // 回退到多次独立交易
+ return e.batchTransferSeparateTransactions(from, privateKey, nonce, items)
+}
+
+// batchTransferSeparateTransactions 执行多次独立的transfer交易
+func (e *ETHNode) batchTransferSeparateTransactions(from string, privateKey *ecdsa.PrivateKey, nonce uint64, items []BatchTransferItem) (*BatchTransferResult, error) {
+ var totalAmount float64
+ var txHashes []string
+ var allSuccess bool = true
+
+ for i, item := range items {
+ // 构造单个transfer交易
+ amountBigInt := utils.Float64ToBigIntUSDT(item.Amount)
+ data, err := e.USDT.ABI.Pack("transfer", common.HexToAddress(strings.ToLower(item.ToAddress)), amountBigInt)
+ if err != nil {
+ log.Printf("❌ 批量转账第%d笔打包失败: %v", i+1, err)
+ allSuccess = false
+ continue
+ }
+
+ // 获取gas limit
+ gasLimit, err := e.getGasLimit()
+ if err != nil {
+ log.Printf("❌ 批量转账第%d笔获取gasLimit失败: %v", i+1, err)
+ allSuccess = false
+ continue
+ }
+
+ // 获取gas费用
+ maxFeePerGas, maxPriorityFeePerGas, err := e.getEIP1559GasFees()
+
+ var txHash string
+ if err != nil {
+ // 回退到传统gas price
+ gasPrice, err := e.getSuggestGasPrice()
+ if err != nil {
+ log.Printf("❌ 批量转账第%d笔获取gasPrice失败: %v", i+1, err)
+ allSuccess = false
+ continue
+ }
+
+ tx := types.NewTransaction(nonce+uint64(i), e.USDT.Address, big.NewInt(0), gasLimit, gasPrice, data)
+ signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey)
+ if err != nil {
+ log.Printf("❌ 批量转账第%d笔签名失败: %v", i+1, err)
+ allSuccess = false
+ continue
+ }
+
+ txHash = signedTx.Hash().Hex()
+ err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
+ if err != nil {
+ log.Printf("❌ 批量转账第%d笔发送失败: %v", i+1, err)
+ allSuccess = false
+ continue
+ }
+ } else {
+ // 使用EIP-1559交易
+ ethBalance, err := e.getETHBlance(from)
+ if err != nil {
+ log.Printf("❌ 批量转账第%d笔获取ETH余额失败: %v", i+1, err)
+ allSuccess = false
+ continue
+ }
+
+ maxGasCost := new(big.Int).Mul(new(big.Int).SetUint64(gasLimit), maxFeePerGas)
+ if ethBalance.Cmp(maxGasCost) == -1 {
+ log.Printf("❌ 批量转账第%d笔ETH余额不足", i+1)
+ allSuccess = false
+ continue
+ }
+
+ tx := types.NewTx(&types.DynamicFeeTx{
+ ChainID: e.NetId,
+ Nonce: nonce + uint64(i),
+ GasTipCap: maxPriorityFeePerGas,
+ GasFeeCap: maxFeePerGas,
+ Gas: gasLimit,
+ To: &e.USDT.Address,
+ Value: big.NewInt(0),
+ Data: data,
+ })
+
+ signedTx, err := types.SignTx(tx, types.NewLondonSigner(e.NetId), privateKey)
+ if err != nil {
+ log.Printf("❌ 批量转账第%d笔签名失败: %v", i+1, err)
+ allSuccess = false
+ continue
+ }
+
+ txHash = signedTx.Hash().Hex()
+ err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
+ if err != nil {
+ log.Printf("❌ 批量转账第%d笔发送失败: %v", i+1, err)
+ allSuccess = false
+ continue
+ }
+ }
+
+ txHashes = append(txHashes, txHash)
+ totalAmount += item.Amount
+ log.Printf("✅ 批量转账第%d笔已提交: %s, 金额=%.2f USDT, 收款地址=%s",
+ i+1, txHash, item.Amount, strings.ToLower(item.ToAddress))
+ }
+
+ log.Printf("📊 批量转账完成: 总计%d笔, 成功%d笔, 总金额=%.2f USDT",
+ len(items), len(txHashes), totalAmount)
+
+ return &BatchTransferResult{
+ TxHash: strings.Join(txHashes, ","),
+ Success: allSuccess && len(txHashes) == len(items),
+ TotalAmount: totalAmount,
+ Count: len(txHashes),
+ }, nil
+}
diff --git a/internal/blockchain/eth/batch_transfer_example.md b/internal/blockchain/eth/batch_transfer_example.md
new file mode 100644
index 0000000..49fc196
--- /dev/null
+++ b/internal/blockchain/eth/batch_transfer_example.md
@@ -0,0 +1,101 @@
+# ERC20-USDT 批量转账功能
+
+## 功能说明
+
+该文件 `batch_transfer.go` 提供了 ERC20-USDT 的批量转账功能,支持从同一个发送地址向多个不同的接收地址转账。
+
+## 主要功能
+
+### 1. 批量转账类型
+
+```go
+type BatchTransferItem struct {
+ ToAddress string // 接收地址
+ Amount float64 // 转账金额
+}
+
+type BatchTransferResult struct {
+ TxHash string // 交易哈希(多个用逗号分隔)
+ Success bool // 是否成功
+ TotalAmount float64 // 总转账金额
+ Count int // 转账笔数
+}
+```
+
+### 2. 使用方法
+
+```go
+// 1. 准备批量转账列表
+items := []eth.BatchTransferItem{
+ {ToAddress: "0xRecipient1", Amount: 100.0},
+ {ToAddress: "0xRecipient2", Amount: 200.0},
+ {ToAddress: "0xRecipient3", Amount: 50.0},
+}
+
+// 2. 调用批量转账
+fromAddress := "0xYourAddress"
+result, err := ethNode.USDTBatchTransfer(fromAddress, items)
+if err != nil {
+ log.Fatalf("批量转账失败: %v", err)
+}
+
+// 3. 处理结果
+fmt.Printf("批量转账完成: %d笔, 总金额: %.2f USDT", result.Count, result.TotalAmount)
+fmt.Printf("交易哈希: %s", result.TxHash)
+```
+
+## 工作原理
+
+由于标准 ERC20 合约不支持批量转账,本实现采用以下策略:
+
+1. **多次独立交易**:对每笔转账创建一个独立的 ERC20 `transfer` 交易
+2. **Nonce 管理**:自动管理 nonce,确保交易按顺序广播
+3. **Gas 费用**:支持 EIP-1559 动态费用和传统 gas price
+4. **错误处理**:单笔失败不影响其他交易,返回成功和失败的详细统计
+
+## 注意事项
+
+### 1. Gas 费用
+
+- 每笔转账需要独立的 gas 费用(约 65,000 gas)
+- 批量转账 10 笔需要约 650,000 gas
+- 确保发送地址有足够的 ETH 作为 gas 费用
+
+### 2. 余额检查
+
+- 函数会自动检查 USDT 余额是否足够
+- 如果余额不足,会返回错误并终止转账
+
+### 3. 部分成功
+
+- 如果某些转账失败,函数会继续执行其他转账
+- 返回结果中包含成功笔数和详细交易哈希
+
+### 4. 网络拥堵
+
+- 在高网络拥堵时,某些交易可能被推迟
+- 建议监控所有交易状态
+
+## 性能优化建议
+
+如果需要更高效的批量转账,考虑:
+
+1. **部署批量转账代理合约**:实现一个合约方法 `batchTransfer(address[] to, uint256[] amounts)`
+2. **使用多签钱包**:减少私钥管理风险
+3. **Gas 优化**:使用更低的 gas price 分批发送
+
+## 示例输出
+
+```
+🔄 批量转账 - 检测钱包=0x...,余额=1000.00 USDT
+✅ 批量转账第1笔已提交: 0xabc123..., 金额=100.00 USDT, 收款地址=0x...
+✅ 批量转账第2笔已提交: 0xdef456..., 金额=200.00 USDT, 收款地址=0x...
+✅ 批量转账第3笔已提交: 0x789ghi..., 金额=50.00 USDT, 收款地址=0x...
+📊 批量转账完成: 总计3笔, 成功3笔, 总金额=350.00 USDT
+```
+
+## 限制
+
+- 标准 ERC20 不支持真正的批量转账(单笔交易)
+- 需要确保发送地址有足够的 ETH 作为 gas 费用
+- 交易按顺序发送,可能在高负载时较慢
diff --git a/internal/blockchain/eth/eth.go b/internal/blockchain/eth/eth.go
index c367c53..82c5884 100644
--- a/internal/blockchain/eth/eth.go
+++ b/internal/blockchain/eth/eth.go
@@ -125,27 +125,25 @@ func NewETHNode(cfg message.ETHConfig, decodeKey string) (*ETHNode, error) {
}
// ============================ 抽象接口 ============================
-func (e *ETHNode) AddAddress(address string, rmq_msg any) {
+func (e *ETHNode) AddAddress(address string, rmq_msg any) error {
// 统一转换为小写
address = strings.ToLower(address)
log.Printf("新增钱包监听消息:%v", rmq_msg)
e.ListenAddresses.Store(address, true)
e.mu.Lock()
- if len(e.RmqMsgs[address]) == 0 {
- e.RmqMsgs[address] = []any{rmq_msg}
- } else {
- e.RmqMsgs[address] = append(e.RmqMsgs[address], rmq_msg)
- }
+ e.RmqMsgs[address] = append(e.RmqMsgs[address], rmq_msg)
e.mu.Unlock()
+ return nil
}
-func (e *ETHNode) RemoveAddress(address string) {
+func (e *ETHNode) RemoveAddress(address string) error {
// 统一转换为小写
address = strings.ToLower(address)
e.ListenAddresses.Delete(address)
e.mu.Lock()
delete(e.RmqMsgs, address)
e.mu.Unlock()
+ return nil
}
func (e *ETHNode) Listen(symbol string, ch chan any) {
@@ -174,6 +172,10 @@ func (e *ETHNode) Transfer(symbol string, msg any) error {
return nil
}
+func (e *ETHNode) Stop() {
+ e.Cancel()
+}
+
// ============================ rpc节点方法 ============================
func (e *ETHNode) getETHBlance(address string) (*big.Int, error) {
@@ -221,12 +223,11 @@ func (e *ETHNode) getUSDTBalance(address string) (float64, error) {
return bal, nil
}
-func (e *ETHNode) getBlockHeight() (uint64, error) {
- header, err := e.RpcClient.HeaderByNumber(e.Ctx, nil)
- if err != nil {
- return 0, fmt.Errorf("failed to get latest block header: %w", err)
- }
- return header.Number.Uint64(), nil
+func (e *ETHNode) getGasLimit() (uint64, error) {
+ // 对于ERC20转账,使用固定的gas limit
+ // 通常ERC20 transfer需要约65,000-100,000 gas
+ // 这里设置为80,000,足够覆盖大部分情况
+ return 80000, nil
}
func (e *ETHNode) getSuggestGasPrice() (*big.Int, error) {
@@ -235,9 +236,56 @@ func (e *ETHNode) getSuggestGasPrice() (*big.Int, error) {
if err != nil {
return nil, fmt.Errorf("get suggest-gasprice error:%v", err)
}
+
+ // 设置gas price上限,避免在网络拥堵时费用过高
+ // 这里设置为20 Gwei (20 * 10^9 wei)
+ maxGasPrice := new(big.Int).SetUint64(20000000000) // 20 Gwei
+
+ if gasPrice.Cmp(maxGasPrice) > 0 {
+ log.Printf("⚠️ 建议gas price过高 (%v Gwei),使用上限 20 Gwei", new(big.Int).Div(gasPrice, big.NewInt(1000000000)))
+ return maxGasPrice, nil
+ }
+
+ log.Printf("✅ 使用建议gas price: %v Gwei", new(big.Int).Div(gasPrice, big.NewInt(1000000000)))
return gasPrice, nil
}
+// getEIP1559GasFees 获取EIP-1559的gas费用参数
+func (e *ETHNode) getEIP1559GasFees() (*big.Int, *big.Int, error) {
+ ctx := context.Background()
+
+ // 获取基础费用
+ latestBlock, err := e.RpcClient.BlockByNumber(ctx, nil)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to get latest block: %w", err)
+ }
+
+ baseFee := latestBlock.BaseFee()
+ if baseFee == nil {
+ return nil, nil, fmt.Errorf("base fee not available")
+ }
+
+ // 设置优先级费用(tip),这里设置为2 Gwei
+ maxPriorityFeePerGas := new(big.Int).SetUint64(2000000000) // 2 Gwei
+
+ // 计算最大费用 = 基础费用 + 优先级费用
+ maxFeePerGas := new(big.Int).Add(baseFee, maxPriorityFeePerGas)
+
+ // 设置最大费用上限为30 Gwei
+ maxFeeLimit := new(big.Int).SetUint64(30000000000) // 30 Gwei
+ if maxFeePerGas.Cmp(maxFeeLimit) > 0 {
+ log.Printf("⚠️ 计算的最大费用过高 (%v Gwei),使用上限 30 Gwei", new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000)))
+ maxFeePerGas = maxFeeLimit
+ }
+
+ log.Printf("✅ EIP-1559 Gas费用: BaseFee=%v Gwei, MaxPriorityFee=%v Gwei, MaxFee=%v Gwei",
+ new(big.Int).Div(baseFee, big.NewInt(1000000000)),
+ new(big.Int).Div(maxPriorityFeePerGas, big.NewInt(1000000000)),
+ new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000)))
+
+ return maxFeePerGas, maxPriorityFeePerGas, nil
+}
+
// ============================ 业务方法 ============================
func (e *ETHNode) listen_usdt(ch chan any) error {
fmt.Println("🔍 ETH 开始监听 USDT Transfer 事件...")
@@ -257,6 +305,7 @@ func (e *ETHNode) listen_usdt(ch chan any) error {
fmt.Println("✅ 订阅成功")
// 处理事件
for {
+
select {
case err := <-sub.Err():
fmt.Println("⚠️ 订阅异常,准备重连:", err)
@@ -578,7 +627,6 @@ func (e *ETHNode) decodePrivatekey(address string) string {
}
// 使用key解密
privateKey := encryptedKey // 实际使用时替换成具体的解密代码
- // fmt.Println(privateKey)
return privateKey
}
@@ -624,7 +672,6 @@ func (e *ETHNode) usdt_transfer(msg any) error {
if originalKey == "" {
return fmt.Errorf("failed to query privatekey")
}
- fmt.Println(originalKey)
privateKey, err := crypto.HexToECDSA(originalKey)
if err != nil {
return fmt.Errorf("failed to parse private key: %w", err)
@@ -640,41 +687,125 @@ func (e *ETHNode) usdt_transfer(msg any) error {
if err != nil {
return fmt.Errorf("failed to pack transfer data: %w", err)
}
- gasPrice, err := e.getSuggestGasPrice() // 获得当前建议gasPrice
+ gasLimit, err := e.getGasLimit() // 获得gasLimit
if err != nil {
- return fmt.Errorf("get suggest-gasprice error:%v", err)
+ return fmt.Errorf("get gas limit error:%v", err)
}
- eth_balance, err := e.getETHBlance(final_from) // 获得钱包eth余额
+
+ // 获取EIP-1559 gas费用参数
+ maxFeePerGas, maxPriorityFeePerGas, err := e.getEIP1559GasFees()
+ if err != nil {
+ log.Printf("⚠️ 获取EIP-1559费用失败,回退到传统gas price: %v", err)
+ // 回退到传统gas price
+ gasPrice, err := e.getSuggestGasPrice()
+ if err != nil {
+ return fmt.Errorf("get suggest-gasprice error:%v", err)
+ }
+
+ eth_balance, err := e.getETHBlance(final_from)
+ if err != nil {
+ return fmt.Errorf("%w", err)
+ }
+
+ gasLimit_b := new(big.Int).SetUint64(gasLimit)
+ gas := new(big.Int).Mul(gasLimit_b, gasPrice)
+
+ // 计算gas费用(以ETH为单位)
+ gasInETH := new(big.Float).SetInt(gas)
+ gasInETH.Quo(gasInETH, new(big.Float).SetInt64(1000000000000000000))
+
+ log.Printf("💰 传统Gas费用预估: Limit=%d, Price=%v Gwei, 总费用=%.6f ETH",
+ gasLimit,
+ new(big.Int).Div(gasPrice, big.NewInt(1000000000)),
+ gasInETH)
+
+ // 判断钱包eth是否支持本次交易gas费用
+ if eth_balance.Cmp(gas) == -1 {
+ ethBalanceInETH := new(big.Float).SetInt(eth_balance)
+ ethBalanceInETH.Quo(ethBalanceInETH, new(big.Float).SetInt64(1000000000000000000))
+ return fmt.Errorf("❌ 地址 %s ETH余额不足: %.6f ETH < %.6f ETH (gas费用)",
+ final_from, ethBalanceInETH, gasInETH)
+ }
+
+ // 构造传统交易
+ tx := types.NewTransaction(
+ nonce,
+ e.USDT.Address,
+ big.NewInt(0),
+ gasLimit,
+ gasPrice,
+ data,
+ )
+
+ // 签名并发送传统交易
+ signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey)
+ if err != nil {
+ return fmt.Errorf("failed to sign transaction: %w", err)
+ }
+
+ txHash := signedTx.Hash().Hex()
+ err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
+ if err != nil {
+ return fmt.Errorf("failed to send transaction: %w", err)
+ }
+
+ log.Printf("✅ 传统交易已提交至mempool:%s,金额:%.2f USDT, 手续费:%.6f ETH", txHash, amount, gasInETH)
+ return nil
+ }
+
+ // 使用EIP-1559交易
+ eth_balance, err := e.getETHBlance(final_from)
if err != nil {
return fmt.Errorf("%w", err)
}
- var gasLimit uint64 = 100000
- gasLimit_b := new(big.Int).SetUint64(gasLimit)
- gas := new(big.Int).Mul(gasLimit_b, gasPrice)
+
+ // 计算最大可能的gas费用
+ maxGasCost := new(big.Int).Mul(new(big.Int).SetUint64(gasLimit), maxFeePerGas)
+
+ // 计算gas费用(以ETH为单位)
+ maxGasCostInETH := new(big.Float).SetInt(maxGasCost)
+ maxGasCostInETH.Quo(maxGasCostInETH, new(big.Float).SetInt64(1000000000000000000))
+
+ log.Printf("💰 EIP-1559 Gas费用预估: Limit=%d, MaxFee=%v Gwei, MaxPriorityFee=%v Gwei, 最大费用=%.6f ETH",
+ gasLimit,
+ new(big.Int).Div(maxFeePerGas, big.NewInt(1000000000)),
+ new(big.Int).Div(maxPriorityFeePerGas, big.NewInt(1000000000)),
+ maxGasCostInETH)
+
// 判断钱包eth是否支持本次交易gas费用
- if eth_balance.Cmp(gas) == -1 {
- return fmt.Errorf("address=%s balance less than gas=%v(wei)", final_from, eth_balance)
+ if eth_balance.Cmp(maxGasCost) == -1 {
+ ethBalanceInETH := new(big.Float).SetInt(eth_balance)
+ ethBalanceInETH.Quo(ethBalanceInETH, new(big.Float).SetInt64(1000000000000000000))
+ return fmt.Errorf("❌ 地址 %s ETH余额不足: %.6f ETH < %.6f ETH (最大gas费用)",
+ final_from, ethBalanceInETH, maxGasCostInETH)
}
- // 构造发送到 USDT 合约地址的交易
- tx := types.NewTransaction(
- nonce,
- e.USDT.Address, // 发送到USDT合约地址
- big.NewInt(0), // value为0(ERC20转账不需要ETH)
- gasLimit, // GasLimit设置为100000(ERC20转账需要更多gas)
- gasPrice, // GasPrice: 20 Gwei
- data, // 附加数据:transfer方法调用
- )
- // 6, 签名交易并获得txHash
- signedTx, err := types.SignTx(tx, types.NewEIP155Signer(e.NetId), privateKey)
- // txHash := signedTx.Hash().Hex() // 通过签名信息解析出交易hash
+
+ // 构造EIP-1559交易
+ tx := types.NewTx(&types.DynamicFeeTx{
+ ChainID: e.NetId,
+ Nonce: nonce,
+ GasTipCap: maxPriorityFeePerGas,
+ GasFeeCap: maxFeePerGas,
+ Gas: gasLimit,
+ To: &e.USDT.Address,
+ Value: big.NewInt(0),
+ Data: data,
+ })
+ // 6, 签名EIP-1559交易并获得txHash
+ signedTx, err := types.SignTx(tx, types.NewLondonSigner(e.NetId), privateKey)
if err != nil {
- return fmt.Errorf("failed to sign transaction: %w", err)
+ return fmt.Errorf("failed to sign EIP-1559 transaction: %w", err)
}
- // 7, 发送交易
+
+ txHash := signedTx.Hash().Hex()
+
+ // 7, 发送EIP-1559交易
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
if err != nil {
- return fmt.Errorf("failed to send transaction: %w", err)
+ return fmt.Errorf("failed to send EIP-1559 transaction: %w", err)
}
+
+ log.Printf("✅ EIP-1559交易已提交至mempool:%s,金额:%.2f USDT, 最大手续费:%.6f ETH", txHash, amount, maxGasCostInETH)
// // 8, 构造交易消息
// tx_msg := message.Tx_msg{
// TxType: tx_type,
@@ -692,7 +823,3 @@ func (e *ETHNode) usdt_transfer(msg any) error {
// e.UnConfirmTxs[txHash] = tx_msg
return nil
}
-
-func (e *ETHNode) Stop() {
- e.Cancel()
-}
diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go
new file mode 100644
index 0000000..4aa42e0
--- /dev/null
+++ b/internal/db/sqlite.go
@@ -0,0 +1,120 @@
+package db
+
+import (
+ "database/sql"
+ "fmt"
+
+ _ "modernc.org/sqlite" // 导入驱动
+)
+
+type SQLite struct {
+ DB *sql.DB
+}
+
+// 初始化连接
+func NewSQLite(path string) (*SQLite, error) {
+ db, err := sql.Open("sqlite", path)
+ if err != nil {
+ return nil, fmt.Errorf("open sqlite failed: %v", err)
+ }
+ return &SQLite{DB: db}, nil
+}
+
+// 关闭数据库
+func (s *SQLite) Close() {
+ if s.DB != nil {
+ s.DB.Close()
+ }
+}
+
+// 通用新建表
+func (s *SQLite) Exec_(sql string) error {
+ _, err := s.DB.Exec(sql)
+ if err != nil {
+ return fmt.Errorf("Exec DB error: %w", err)
+ }
+ return nil
+}
+
+// 通用查询方法(返回[]map[string]any)
+func (s *SQLite) Query_(query string, args ...any) ([]map[string]any, error) {
+ rows, err := s.DB.Query(query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("query error: %v", err)
+ }
+ defer rows.Close()
+
+ columns, err := rows.Columns()
+ if err != nil {
+ return nil, err
+ }
+
+ var results []map[string]any
+ for rows.Next() {
+ // 创建一个与列数相同的 slice 用于 Scan
+ values := make([]any, len(columns))
+ valuePtrs := make([]any, len(columns))
+ for i := range values {
+ valuePtrs[i] = &values[i]
+ }
+
+ if err := rows.Scan(valuePtrs...); err != nil {
+ return nil, err
+ }
+
+ // 将行转换为 map
+ rowMap := make(map[string]any)
+ for i, col := range columns {
+ val := values[i]
+ if b, ok := val.([]byte); ok {
+ rowMap[col] = string(b)
+ } else {
+ rowMap[col] = val
+ }
+ }
+ results = append(results, rowMap)
+ }
+ return results, nil
+}
+
+// 插入数据(支持事务和批量)
+func (s *SQLite) Insert(sqlStr string, args ...[]any) error {
+ tx, err := s.DB.Begin()
+ if err != nil {
+ return err
+ }
+ stmt, err := tx.Prepare(sqlStr)
+ if err != nil {
+ return err
+ }
+ defer stmt.Close()
+
+ for _, a := range args {
+ _, err = stmt.Exec(a...)
+ if err != nil {
+ tx.Rollback()
+ return fmt.Errorf("exec insert error: %v", err)
+ }
+ }
+ return tx.Commit()
+}
+
+// 更新数据
+func (s *SQLite) Update(sqlStr string, args ...any) (int64, error) {
+ res, err := s.DB.Exec(sqlStr, args...)
+ if err != nil {
+ return 0, fmt.Errorf("update error: %v", err)
+ }
+ rows, _ := res.RowsAffected()
+ return rows, nil
+}
+
+// 删除数据
+func (s *SQLite) Delete(sqlStr string, args ...any) (int64, error) {
+ res, err := s.DB.Exec(sqlStr, args...)
+ if err != nil {
+ return 0, fmt.Errorf("delete error: %v", err)
+ }
+ rows, _ := res.RowsAffected()
+ return rows, nil
+}
diff --git a/internal/logger/transaction_logger.go b/internal/logger/transaction_logger.go
index 74300bf..885d6fc 100644
--- a/internal/logger/transaction_logger.go
+++ b/internal/logger/transaction_logger.go
@@ -188,20 +188,20 @@ func compressFile(filePath string) error {
}
// LogTopup 记录充值消息
-func LogTopup(address string, status string, amount float64, txHash string, blockHeight uint64) {
+func LogTopup(toAddress string, status string, amount float64, txHash string, blockHeight uint64) {
if txLogger == nil {
return
}
- lf, err := txLogger.getOrCreateLogFile(address)
+ lf, err := txLogger.getOrCreateLogFile(toAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
- content := fmt.Sprintf("%s [topup]-[%s] | 金额: %.6f | 交易哈希: %s | 区块高度: %d | 地址: %s",
- timestamp, status, amount, txHash, blockHeight, address)
+ content := fmt.Sprintf("%s [topup]-[%s] | 金额: %.6f | 交易哈希: %s | 区块高度: %d | ToAddress: %s",
+ timestamp, status, amount, txHash, blockHeight, toAddress)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
@@ -209,21 +209,21 @@ func LogTopup(address string, status string, amount float64, txHash string, bloc
}
// LogWithdraw 记录提现消息
-func LogWithdraw(queueId string, status string, amount float64, from string, to string, txHash string, blockHeight uint64) {
+func LogWithdraw(toAddress string, status string, amount float64, fromAddress string, txHash string, blockHeight uint64) {
if txLogger == nil {
return
}
- // 使用 queueId 作为文件名
- lf, err := txLogger.getOrCreateLogFile(queueId)
+ // 使用 toAddress 作为文件名
+ lf, err := txLogger.getOrCreateLogFile(toAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
- content := fmt.Sprintf("%s [withdraw]-[%s] | 金额: %.6f | From: %s | To: %s | 交易哈希: %s | 区块高度: %d",
- timestamp, status, amount, from, to, txHash, blockHeight)
+ content := fmt.Sprintf("%s [withdraw]-[%s] | 金额: %.6f | FromAddress: %s | ToAddress: %s | 交易哈希: %s | 区块高度: %d",
+ timestamp, status, amount, fromAddress, toAddress, txHash, blockHeight)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
@@ -231,21 +231,21 @@ func LogWithdraw(queueId string, status string, amount float64, from string, to
}
// LogPay 记录支付消息
-func LogPay(orderId string, queueId string, status string, amount float64, from string, to string, txHash string, blockHeight uint64) {
+func LogPay(toAddress string, status string, amount float64, fromAddress string, txHash string, blockHeight uint64, orderId string, queueId string) {
if txLogger == nil {
return
}
- // 使用 orderId 作为文件名
- lf, err := txLogger.getOrCreateLogFile(orderId)
+ // 使用 toAddress 作为文件名
+ lf, err := txLogger.getOrCreateLogFile(toAddress)
if err != nil {
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
return
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
- content := fmt.Sprintf("%s [pay]-[%s] | 订单ID: %s | 队列ID: %s | 金额: %.6f | From: %s | To: %s | 交易哈希: %s | 区块高度: %d",
- timestamp, status, orderId, queueId, amount, from, to, txHash, blockHeight)
+ content := fmt.Sprintf("%s [pay]-[%s] | 金额: %.6f | FromAddress: %s | ToAddress: %s | 交易哈希: %s | 区块高度: %d | OrderId: %s | QueueId: %s",
+ timestamp, status, amount, fromAddress, toAddress, txHash, blockHeight, orderId, queueId)
if err := lf.write(content); err != nil {
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
diff --git a/internal/msg/msg.go b/internal/msg/msg.go
index 8d8add0..c70a373 100644
--- a/internal/msg/msg.go
+++ b/internal/msg/msg.go
@@ -4,19 +4,26 @@ import "time"
// 配置文件结构
type Config struct {
+ SQLite3 SQLite3 `json:"sqlite3"`
RMQConfig RMQConfig `json:"rmq_config"`
ETHConfig ETHConfig `json:"eth_config"`
TRONConfig TRONConfig `json:"tron_config"`
}
+type SQLite3 struct {
+ MsgPath string `json:"msg_path"`
+}
+
type RMQConfig struct {
SubAddr string `json:"sub_addr"` // 监听地址
PayConfig QueueConfig `json:"pay"` // 支付
TopUpConfig QueueConfig `json:"topup"` // 充值
WithdrawConfig QueueConfig `json:"withdraw"` // 提现
+ RemoveConfig QueueConfig `json:"remove"` // 移除监听
PayRespConfig QueueConfig `json:"pay_resp"` // 支付回复
TopUpRespConfig QueueConfig `json:"topup_resp"` // 充值回复
WithdrawRespConfig QueueConfig `json:"withdraw_resp"` // 提现回复
+ RemoveRespConfig QueueConfig `json:"remove_resp"` // 移除监听回复
}
type QueueConfig struct {
@@ -50,7 +57,7 @@ type DbConfig struct {
ConnMaxLife time.Duration `json:"connMaxLife"` // 连接最大存活时间
}
-// =======================================================================
+// =============================== type0 ===============================
// 接收的充值消息
type TopupMsg_req struct {
Chain string `json:"chain"` // 链名称
@@ -71,6 +78,7 @@ type TopupMsg_resp struct {
BlockHeight uint64 `json:"block_height"` // 区块高度
}
+// =============================== type1 ===============================
// 接收的提现消息
type WithdrawMsg_req struct {
QueueId string `json:"queue_id"`
@@ -86,16 +94,17 @@ type WithdrawMsg_req struct {
// 返回提现结果消息
type WithdrawMsg_resp struct {
QueueId string `json:"queue_id"`
- Status int `json:"status"`
- Amount float64 `json:"amount"`
Chain string `json:"chain"` // 链名称
Symbol string `json:"symbol"` // 币种
+ Status int `json:"status"`
+ Amount float64 `json:"amount"`
TxHash string `json:"tx_hash"`
FromAddress string `json:"from_address"` // 来源地址
ToAddress string `json:"to_address"` // 目标地址
BlockHeight uint64 `json:"block_height"` // 区块高度
}
+// =============================== type2 ===============================
// 接收到的支付消息
type PayMsg_req struct {
QueueId string `json:"queue_id"`
@@ -123,6 +132,27 @@ type PayMsg_resp struct {
BlockHeight uint64 `json:"block_height"` // 区块高度
}
+// =============================== type3 ===============================
+// 接收到的删除监听地址消息
+type RemoveListenMsg_req struct {
+ MsgType int `json:"msg_type"`
+ Chain string `json:"chain"`
+ Symbol string `json:"symbol"`
+ Address string `json:"address"`
+ Timestamp uint64 `json:"timestamp"`
+ Sign string `json:"sign"`
+}
+
+// 返回收到的删除监听地址消息
+type RemoveListenMsg_resp struct {
+ MsgType int `json:"msg_type"`
+ Chain string `json:"chain"`
+ Symbol string `json:"symbol"`
+ Address string `json:"address"`
+ Status int `json:"status"` // 0失败 1成功
+}
+
+// =====================================================================
// 节点通用消息结构
type Tx_msg struct {
TxType int `json:"tx_type"` // 转账类型:0充值,1提现,2支付
diff --git a/internal/server.go b/internal/server.go
index 90be55c..8375464 100644
--- a/internal/server.go
+++ b/internal/server.go
@@ -8,6 +8,7 @@ import (
"m2pool-payment/internal/blockchain"
"m2pool-payment/internal/blockchain/eth"
"m2pool-payment/internal/crypto"
+ "m2pool-payment/internal/db"
"m2pool-payment/internal/logger"
message "m2pool-payment/internal/msg"
rmq "m2pool-payment/internal/queue"
@@ -15,6 +16,7 @@ import (
"os/signal"
"strings"
"syscall"
+ "time"
)
const MSG_KEY string = "9f3c7a12"
@@ -32,6 +34,7 @@ type ServerCtx struct {
Config message.Config
blockChainServer *blockchain.BlockChainServer
rmqServer *rmq.RabbitMQServer
+ sqlitedb db.SQLite
}
var s_ctx ServerCtx
@@ -76,6 +79,113 @@ func initBlockChainServer() {
log.Println("✅ 区块链服务初始化完成")
}
+func loadSQLiteData() {
+ err1 := loadTopupReqMsg()
+ if err1 != nil {
+ log.Fatalf("load topup msg err:%v", err1)
+ }
+ err2 := loadWithdrawReqMsg()
+ if err2 != nil {
+ log.Fatalf("load withdraw msg err:%v", err2)
+ }
+ err3 := loadPayReqMsg()
+ if err3 != nil {
+ log.Fatalf("load pay msg err:%v", err3)
+ }
+}
+
+func loadTopupReqMsg() error {
+ sql := `SELECT chain, symbol, timestamp, to_addr FROM msg_topup_req;`
+ rows, err := s_ctx.sqlitedb.DB.Query(sql)
+ if err != nil {
+ return fmt.Errorf("query history topup-msg error: %w", err)
+ }
+ defer rows.Close()
+
+ var topupReq_msg message.TopupMsg_req
+ hasData := false
+ for rows.Next() {
+ hasData = true
+ if err := rows.Scan(&topupReq_msg.Chain, &topupReq_msg.Symbol, &topupReq_msg.Timestamp, &topupReq_msg.Address); err != nil {
+ return err
+ }
+ s_ctx.blockChainServer.AddAddress(topupReq_msg.Chain, topupReq_msg.Address, topupReq_msg)
+ }
+
+ if !hasData {
+ log.Println("Msg_topup_req`s msg has not data, doesn`t need to load.")
+ return nil
+ }
+
+ // 在遍历完所有数据后检查是否发生了错误
+ if err := rows.Err(); err != nil {
+ log.Printf("Error encountered while iterating over rows: %v", err)
+ }
+ return nil
+}
+
+func loadWithdrawReqMsg() error {
+ sql := `SELECT queueId, chain, symbol, timestamp, from_addr, to_addr, amount FROM msg_withdraw_req;`
+ rows, err := s_ctx.sqlitedb.DB.Query(sql)
+ if err != nil {
+ return fmt.Errorf("query history withdraw-msg error: %w", err)
+ }
+ defer rows.Close()
+
+ var withdrawReq_msg message.WithdrawMsg_req
+ hasData := false
+ for rows.Next() {
+ hasData = true
+ // var chain, symbol, to_addr string
+ // var timestamp uint64
+ if err := rows.Scan(&withdrawReq_msg.QueueId, &withdrawReq_msg.Chain, &withdrawReq_msg.Symbol, &withdrawReq_msg.Timestamp, &withdrawReq_msg.FromAddress, &withdrawReq_msg.ToAddress, &withdrawReq_msg.Amount); err != nil {
+ return err
+ }
+ s_ctx.blockChainServer.AddAddress(withdrawReq_msg.Chain, withdrawReq_msg.ToAddress, withdrawReq_msg)
+ }
+
+ if !hasData {
+ log.Println("Msg_withdraw_req`s msg has not data, doesn`t need to load.")
+ return nil
+ }
+
+ // 在遍历完所有数据后检查是否发生了错误
+ if err := rows.Err(); err != nil {
+ log.Printf("Error encountered while iterating over rows: %v", err)
+ }
+ return nil
+}
+
+func loadPayReqMsg() error {
+ sql := `SELECT queueId, chain, symbol, timestamp, from_addr, to_addr, amount, orderId FROM msg_pay_req;`
+ rows, err := s_ctx.sqlitedb.DB.Query(sql)
+ if err != nil {
+ return fmt.Errorf("query history pay-msg error: %w", err)
+ }
+ defer rows.Close()
+
+ var payReq_msg message.PayMsg_req
+ hasData := false
+ for rows.Next() {
+ hasData = true
+ if err := rows.Scan(&payReq_msg.QueueId, &payReq_msg.Chain, &payReq_msg.Symbol, &payReq_msg.Timestamp, &payReq_msg.FromAddress, &payReq_msg.ToAddress, &payReq_msg.Amount, &payReq_msg.OrderId); err != nil {
+ return err
+ }
+ s_ctx.blockChainServer.AddAddress(payReq_msg.Chain, payReq_msg.ToAddress, payReq_msg)
+ }
+
+ if !hasData {
+ log.Println("Msg_pay_req`s msg has not data, doesn`t need to load.")
+ return nil
+ }
+
+ // 在遍历完所有数据后检查是否发生了错误
+ if err := rows.Err(); err != nil {
+ log.Printf("Error encountered while iterating over rows: %v", err)
+ }
+ return nil
+}
+
func initRmqServer() {
// 初始化rmq服务
rmq_server, err := rmq.NewRabbitMQServer(s_ctx.Config.RMQConfig)
@@ -88,6 +198,21 @@ func initRmqServer() {
log.Printf("✅ RabbitMQ服务初始化完成: %s", s_ctx.Config.RMQConfig.SubAddr)
}
+func initSQLite(sqlite3_file string) {
+ // 初始化sqlite3数据库
+ sqlite3, err := db.NewSQLite(sqlite3_file)
+ if err != nil {
+ log.Fatalf("connect sqlite3 error:%v", err)
+ return
+ }
+ sqlByte, err := os.ReadFile("../public/SQLite3.sql")
+ if err != nil {
+ log.Fatalf("open sql file error: %s", "../public/SQLite3.sql")
+ }
+ sqlite3.Exec_(string(sqlByte))
+ s_ctx.sqlitedb = *sqlite3
+}
+
func handleTopupMsg() {
s_ctx.rmqServer.OnTopupMsg = func(msg message.TopupMsg_req) {
msg.Address = strings.ToLower(msg.Address)
@@ -109,7 +234,32 @@ func handleTopupMsg() {
}
// 添加监听地址
- s_ctx.blockChainServer.AddAddress(msg.Chain, msg.Address, msg)
+ // go func() {
+ err := s_ctx.blockChainServer.AddAddress(msg.Chain, msg.Address, msg)
+ if err != nil {
+ log.Printf("❌ 添加监听地址失败: %v", err)
+ // 发送失败响应
+ err = s_ctx.rmqServer.PublishTopupResp(message.TopupMsg_resp{
+ Address: msg.Address,
+ Status: STATUS_FAILED,
+ Chain: msg.Chain,
+ Symbol: msg.Symbol,
+ Amount: 0,
+ TxHash: "",
+ })
+ if err != nil {
+ log.Printf("❌ 发布充值失败响应失败: %v", err)
+ }
+ return
+ }
+ // }()
+ // 将新增数据写入sqlite
+ insert_sql := `INSERT OR REPLACE INTO msg_topup_req (chain, symbol, timestamp, to_addr) VALUES (?, ?, ?, ?)`
+ data := []any{msg.Chain, msg.Symbol, msg.Timestamp, msg.Address}
+ err = s_ctx.sqlitedb.Insert(insert_sql, data)
+ if err != nil {
+ log.Printf("❌ 插入 msg_req 失败: %v, data: %+v", err, data)
+ }
}
}
@@ -147,6 +297,30 @@ func handleWithdrawMsg() {
Symbol: msg.Symbol,
TxHash: "",
})
+ return // 转账失败时直接返回,不进入链上确认流程
+ }
+ // go func() {
+ err = s_ctx.blockChainServer.AddAddress(msg.Chain, msg.ToAddress, msg)
+ if err != nil {
+ log.Printf("❌ 添加监听地址失败: %v", err)
+ // 发送失败响应
+ s_ctx.rmqServer.PublishWithdrawResp(message.WithdrawMsg_resp{
+ QueueId: msg.QueueId,
+ Status: STATUS_FAILED,
+ Amount: msg.Amount,
+ Chain: msg.Chain,
+ Symbol: msg.Symbol,
+ TxHash: "",
+ })
+ return
+ }
+ // }()
+ // 将新增数据写入sqlite
+ insert_sql := `INSERT OR REPLACE INTO msg_withdraw_req (queueId, chain, symbol, timestamp, from_addr, to_addr, amount) VALUES (?, ?, ?, ?, ?, ?, ?)`
+ data := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Timestamp, msg.FromAddress, msg.ToAddress, msg.Amount}
+ err = s_ctx.sqlitedb.Insert(insert_sql, data)
+ if err != nil {
+ log.Printf("❌ 插入 withdraw_req 失败: %v, data: %+v", err, data)
}
}
}
@@ -187,6 +361,31 @@ func handlePayMsg() {
OrderId: msg.OrderId,
TxHash: "",
})
+ return // 转账失败时直接返回,不进入链上确认流程
+ }
+ // go func() {
+ err = s_ctx.blockChainServer.AddAddress(msg.Chain, msg.ToAddress, msg)
+ if err != nil {
+ log.Printf("❌ 添加监听地址失败: %v", err)
+ // 发送失败响应
+ s_ctx.rmqServer.PublishPayResp(message.PayMsg_resp{
+ QueueId: msg.QueueId,
+ Status: STATUS_FAILED,
+ Amount: msg.Amount,
+ Chain: msg.Chain,
+ Symbol: msg.Symbol,
+ OrderId: msg.OrderId,
+ TxHash: "",
+ })
+ return
+ }
+ // }()
+ // 将新增数据写入sqlite
+ insert_sql := `INSERT OR REPLACE INTO msg_pay_req (queueId, chain, symbol, timestamp, from_addr, to_addr, amount, orderId) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
+ data := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Timestamp, msg.FromAddress, msg.ToAddress, msg.Amount, msg.OrderId}
+ err = s_ctx.sqlitedb.Insert(insert_sql, data)
+ if err != nil {
+ log.Printf("❌ 插入 pay_req 失败: %v, data: %+v", err, data)
}
}
}
@@ -233,32 +432,87 @@ func handleChainEvent(chainEventCh chan any) {
err := s_ctx.rmqServer.PublishTopupResp(msg)
if err != nil {
log.Printf("❌ 发送充值响应失败: %v", err)
+ return
}
+ go func() {
+ // 插入响应数据
+ sql := `INSERT INTO msg_topup_resp (chain, symbol, timestamp, to_addr, amount, height, txHash, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
+ data := []any{msg.Chain, msg.Symbol, time.Now().Unix(), msg.Address, msg.Amount, msg.BlockHeight, msg.TxHash, msg.Status}
+ err := s_ctx.sqlitedb.Insert(sql, data)
+ if err != nil {
+ log.Printf("❌ 插入 topup_resp 失败: %v", err)
+ return
+ }
+ }()
case message.WithdrawMsg_resp:
// 提现确认
log.Printf("✅ [链上] 提现确认: QueueId=%s, Amount=%.2f, TxHash=%s, Status=%d",
msg.QueueId, msg.Amount, msg.TxHash, msg.Status)
// 记录交易日志
- logger.LogWithdraw(msg.QueueId, "确认", msg.Amount, msg.FromAddress,
- msg.ToAddress, msg.TxHash, msg.BlockHeight)
+ logger.LogWithdraw(msg.ToAddress, "确认", msg.Amount, msg.FromAddress, msg.TxHash, msg.BlockHeight)
err := s_ctx.rmqServer.PublishWithdrawResp(msg)
if err != nil {
log.Printf("❌ 发送提现响应失败: %v", err)
+ return
}
+ go func() {
+ // 插入响应数据
+ sql := `INSERT INTO msg_withdraw_resp (queueId, chain, symbol, timestamp, from_addr, to_addr, amount, height, txHash, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
+ data := []any{msg.QueueId, msg.Chain, msg.Symbol, time.Now().Unix(), msg.FromAddress, msg.ToAddress, msg.Amount, msg.BlockHeight, msg.TxHash, msg.Status}
+ err := s_ctx.sqlitedb.Insert(sql, data)
+ if err != nil {
+ log.Printf("❌ 插入 withdraw_resp 失败: %v", err)
+ return
+ }
+
+ // 删除对应数据
+ del_sql := `DELETE FROM msg_withdraw_req WHERE queueId = ?;`
+ count, err := s_ctx.sqlitedb.Delete(del_sql, msg.QueueId)
+ if err != nil {
+ log.Printf("❌ 清理 withdraw_req 失败: %v, queueId=%s", err, msg.QueueId)
+ } else if count == 0 {
+ log.Printf("⚠️ 未找到要删除的 withdraw_req 记录: queueId=%s", msg.QueueId)
+ } else {
+ log.Printf("✅ 清理 withdraw_req 成功: 删除了 %d 条记录, queueId=%s", count, msg.QueueId)
+ }
+ }()
+
case message.PayMsg_resp:
// 支付确认
log.Printf("✅ [链上] 支付确认: QueueId=%s, OrderId=%s, Amount=%.2f, TxHash=%s, Status=%d",
msg.QueueId, msg.OrderId, msg.Amount, msg.TxHash, msg.Status)
// 记录交易日志
- logger.LogPay(msg.OrderId, msg.QueueId, "确认", msg.Amount, msg.FromAddress,
- msg.ToAddress, msg.TxHash, msg.BlockHeight)
+ logger.LogPay(msg.ToAddress, "确认", msg.Amount, msg.FromAddress, msg.TxHash, msg.BlockHeight, msg.OrderId, msg.QueueId)
err := s_ctx.rmqServer.PublishPayResp(msg)
if err != nil {
log.Printf("❌ 发送支付响应失败: %v", err)
+ return
}
+ go func() {
+ // 插入响应数据
+ sql := `INSERT INTO msg_pay_resp (queueId, chain, symbol, timestamp, from_addr, to_addr, amount, height, txHash, status, orderId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
+ data := []any{msg.QueueId, msg.Chain, msg.Symbol, time.Now().Unix(), msg.FromAddress, msg.ToAddress, msg.Amount, msg.BlockHeight, msg.TxHash, msg.Status, msg.OrderId}
+ err := s_ctx.sqlitedb.Insert(sql, data)
+ if err != nil {
+ log.Printf("❌ 插入 pay_resp 失败: %v", err)
+ return
+ }
+
+ // 删除对应数据
+ del_sql := `DELETE FROM msg_pay_req WHERE queueId = ?;`
+ count, err := s_ctx.sqlitedb.Delete(del_sql, msg.QueueId)
+ if err != nil {
+ log.Printf("❌ 清理 pay_req 失败: %v, queueId=%s", err, msg.QueueId)
+ } else if count == 0 {
+ log.Printf("⚠️ 未找到要删除的 pay_req 记录: queueId=%s", msg.QueueId)
+ } else {
+ log.Printf("✅ 清理 pay_req 成功: 删除了 %d 条记录, queueId=%s", count, msg.QueueId)
+ }
+ }()
+
default:
log.Printf("⚠️ 未知消息类型: %T", event)
}
@@ -283,6 +537,11 @@ func Start(msgKey string) {
// ================== 初始化区块链节点 ==================
initBlockChainServer()
+ // ================== 初始化SQLite3 ==================
+ initSQLite(s_ctx.Config.SQLite3.MsgPath)
+ // 读取历史信息
+ loadSQLiteData()
+
// ================== 初始化 RabbitMQ 服务 ==================
initRmqServer()
diff --git a/public/SQLite3.sql b/public/SQLite3.sql
new file mode 100644
index 0000000..9cff7b2
--- /dev/null
+++ b/public/SQLite3.sql
@@ -0,0 +1,71 @@
+CREATE TABLE IF NOT EXISTS msg_topup_req (
+ chain TEXT,
+ symbol TEXT,
+ timestamp INTEGER,
+ to_addr TEXT,
+ PRIMARY KEY(to_addr)
+);
+
+CREATE TABLE IF NOT EXISTS msg_withdraw_req (
+ queueId TEXT,
+ chain TEXT,
+ symbol TEXT,
+ timestamp INTEGER,
+ from_addr TEXT,
+ to_addr TEXT,
+ amount NUMERIC,
+ PRIMARY KEY(queueId)
+);
+
+CREATE TABLE IF NOT EXISTS msg_pay_req (
+ queueId TEXT,
+ chain TEXT,
+ symbol TEXT,
+ timestamp INTEGER,
+ from_addr TEXT,
+ to_addr TEXT,
+ amount NUMERIC,
+ orderId TEXT,
+ PRIMARY KEY(queueId)
+);
+
+CREATE TABLE IF NOT EXISTS msg_topup_resp (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ chain TEXT,
+ symbol TEXT,
+ timestamp INTEGER,
+ to_addr TEXT,
+ amount NUMERIC,
+ height INTEGER,
+ TxHash TEXT,
+ status INTEGER
+);
+
+CREATE TABLE IF NOT EXISTS msg_withdraw_resp (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ queueId TEXT,
+ chain TEXT,
+ symbol TEXT,
+ timestamp INTEGER,
+ from_addr TEXT,
+ to_addr TEXT,
+ amount NUMERIC,
+ height INTEGER,
+ txHash TEXT,
+ status INTEGER
+);
+
+CREATE TABLE IF NOT EXISTS msg_pay_resp (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ queueId TEXT,
+ chain TEXT,
+ symbol TEXT,
+ timestamp INTEGER,
+ from_addr TEXT,
+ to_addr TEXT,
+ amount NUMERIC,
+ height INTEGER,
+ txHash TEXT,
+ orderId TEXT,
+ status INTEGER
+);
\ No newline at end of file