Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8201356476 | ||
|
|
74d9a114c0 | ||
|
|
ac22db02f3 | ||
|
|
245c9c94cb | ||
|
|
00389efb75 | ||
|
|
8d7da5d345 | ||
|
|
056bc05b75 | ||
|
|
87d8a6313c | ||
|
|
b1d3e07c36 | ||
|
|
b7c84fd101 |
13
go.mod
13
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 (
|
||||
@@ -30,12 +39,14 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
||||
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect
|
||||
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
|
||||
)
|
||||
|
||||
53
go.sum
53
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=
|
||||
@@ -111,10 +117,12 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/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=
|
||||
|
||||
128
integration/wordpress/INSTALL.md
Normal file
128
integration/wordpress/INSTALL.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# 安装指南
|
||||
|
||||
## 前置要求
|
||||
|
||||
1. WordPress 5.0 或更高版本
|
||||
2. WooCommerce 3.0 或更高版本
|
||||
3. PHP 7.4 或更高版本
|
||||
4. M2Pool 支付系统后端(Go 服务)
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 安装 WordPress 插件
|
||||
|
||||
1. 将 `integration/wordpress` 文件夹复制到 WordPress 的插件目录:
|
||||
```
|
||||
wp-content/plugins/m2pool-eth-payment/
|
||||
```
|
||||
|
||||
2. 在 WordPress 后台激活插件:
|
||||
- 进入 `插件 > 已安装的插件`
|
||||
- 找到 "M2Pool ETH Payment Gateway"
|
||||
- 点击 "启用"
|
||||
|
||||
### 2. 配置插件
|
||||
|
||||
1. 进入 `设置 > M2Pool ETH 支付`
|
||||
2. 配置以下选项:
|
||||
- **API 地址**: 支付系统的 API 地址(如果使用 API Bridge,则为 `http://localhost:8080`)
|
||||
- **API 密钥**: 与后端系统的 `msgKey` 对应的密钥
|
||||
- **接收地址**: 用于接收支付的以太坊地址
|
||||
- **监听间隔**: 检查支付状态的间隔时间(建议 30-60 秒)
|
||||
|
||||
3. 保存设置
|
||||
|
||||
### 3. 配置 WooCommerce 支付网关
|
||||
|
||||
1. 进入 `WooCommerce > 设置 > 支付`
|
||||
2. 找到 "M2Pool ETH 支付"
|
||||
3. 点击 "管理"
|
||||
4. 启用支付方式并配置:
|
||||
- **标题**: 客户看到的支付方式名称
|
||||
- **描述**: 支付方式说明
|
||||
- **发送地址**: 发送支付的地址(可选)
|
||||
- **接收地址**: 接收支付的地址
|
||||
- **链名称**: 选择 ETH
|
||||
- **代币符号**: 选择 ETH 或 USDT
|
||||
|
||||
5. 保存更改
|
||||
|
||||
### 4. 设置 API Bridge(可选)
|
||||
|
||||
如果您的后端系统只支持 RabbitMQ,需要运行 API Bridge 服务:
|
||||
|
||||
1. 进入 `integration/wordpress/api-bridge` 目录
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
3. 编译:
|
||||
```bash
|
||||
go build -o api-bridge main.go
|
||||
```
|
||||
4. 运行:
|
||||
```bash
|
||||
./api-bridge
|
||||
```
|
||||
|
||||
5. 在 WordPress 插件设置中,将 API 地址设置为 `http://localhost:8080`
|
||||
|
||||
## 测试支付
|
||||
|
||||
1. 在 WooCommerce 创建一个测试订单
|
||||
2. 选择 "ETH 支付" 作为支付方式
|
||||
3. 完成订单后,您会看到支付说明页面
|
||||
4. 向显示的地址支付指定金额的 ETH
|
||||
5. 系统会自动检测支付并更新订单状态
|
||||
|
||||
## Webhook 配置(推荐)
|
||||
|
||||
为了实时接收支付状态更新,建议配置 Webhook:
|
||||
|
||||
1. 在 WordPress 插件设置页面找到 Webhook URL
|
||||
2. 将此 URL 配置到您的支付系统中
|
||||
3. 当支付状态更新时,系统会自动通知 WordPress
|
||||
|
||||
Webhook URL 格式:
|
||||
```
|
||||
https://your-site.com/wp-json/m2pool-eth/v1/webhook
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 支付状态不更新
|
||||
|
||||
1. 检查 API 地址是否正确
|
||||
2. 检查 API 密钥是否匹配
|
||||
3. 查看 WordPress 错误日志
|
||||
4. 确认后端服务正常运行
|
||||
|
||||
### 无法创建支付请求
|
||||
|
||||
1. 检查 API Bridge 是否运行(如果使用)
|
||||
2. 检查 RabbitMQ 连接是否正常
|
||||
3. 查看后端服务日志
|
||||
|
||||
### 订单状态不更新
|
||||
|
||||
1. 检查定时任务是否运行:
|
||||
- 进入 `工具 > 计划任务`
|
||||
- 查找 `m2pool_eth_check_payments`
|
||||
2. 手动触发支付检查(通过 AJAX)
|
||||
3. 检查数据库表 `wp_m2pool_eth_payments` 中的数据
|
||||
|
||||
## 数据库表
|
||||
|
||||
插件会自动创建以下数据库表:
|
||||
|
||||
- `wp_m2pool_eth_payments`: 存储支付记录
|
||||
|
||||
您可以通过 phpMyAdmin 或 WordPress 数据库工具查看和管理这些表。
|
||||
|
||||
## 支持
|
||||
|
||||
如有问题,请查看:
|
||||
- README.md - 详细文档
|
||||
- WordPress 错误日志
|
||||
- 后端服务日志
|
||||
|
||||
202
integration/wordpress/README.md
Normal file
202
integration/wordpress/README.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# M2Pool ETH Payment Gateway for WordPress
|
||||
|
||||
WordPress 支付网关插件,支持以太坊 (ETH) 交易的支付、监听和返回支付结果。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 集成 WooCommerce 支付网关
|
||||
- ✅ 支持 ETH 和 USDT 支付
|
||||
- ✅ 自动监听支付状态
|
||||
- ✅ 支持 Webhook 回调
|
||||
- ✅ 支付状态实时更新
|
||||
- ✅ 完整的订单管理
|
||||
|
||||
## 安装要求
|
||||
|
||||
- WordPress 5.0+
|
||||
- WooCommerce 3.0+
|
||||
- PHP 7.4+
|
||||
|
||||
## 安装步骤
|
||||
|
||||
1. 将插件文件夹上传到 `/wp-content/plugins/` 目录
|
||||
2. 在 WordPress 后台激活插件
|
||||
3. 进入 `设置 > M2Pool ETH 支付` 配置 API 地址和密钥
|
||||
4. 在 `WooCommerce > 设置 > 支付` 中启用并配置支付网关
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 基本设置
|
||||
|
||||
1. **API 地址**: 支付系统的 API 地址(例如: `http://localhost:8080`)
|
||||
2. **API 密钥**: 用于签名验证的密钥(与后端系统的 `msgKey` 对应)
|
||||
3. **接收地址**: 用于接收支付的以太坊地址
|
||||
4. **监听间隔**: 检查支付状态的间隔时间(秒)
|
||||
|
||||
### Webhook 配置
|
||||
|
||||
Webhook URL: `https://your-site.com/wp-json/m2pool-eth/v1/webhook`
|
||||
|
||||
将此 URL 配置到您的支付系统中,以便接收支付状态更新。
|
||||
|
||||
## API 接口说明
|
||||
|
||||
插件需要与后端支付系统通信。如果后端系统只支持 RabbitMQ,您需要创建一个中间 API 服务。
|
||||
|
||||
### 需要的 API 接口
|
||||
|
||||
#### 1. 创建支付请求
|
||||
|
||||
```
|
||||
POST /api/payment/create
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{
|
||||
"queue_id": "wp_123_1234567890",
|
||||
"chain": "ETH",
|
||||
"symbol": "ETH",
|
||||
"from_address": "0x...",
|
||||
"to_address": "0x...",
|
||||
"amount": 0.1,
|
||||
"fee": 0,
|
||||
"timestamp": 1234567890,
|
||||
"sign": "signature_hash"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"queue_id": "wp_123_1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 查询支付状态
|
||||
|
||||
```
|
||||
GET /api/payment/status/{queue_id}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"queue_id": "wp_123_1234567890",
|
||||
"status": 1,
|
||||
"tx_hash": "0x...",
|
||||
"block_height": 12345,
|
||||
"amount": 0.1
|
||||
}
|
||||
```
|
||||
|
||||
状态码说明:
|
||||
- `0`: 待支付
|
||||
- `1`: 支付成功
|
||||
- `2`: 待确认
|
||||
- `3`: 支付失败
|
||||
|
||||
## 中间 API 服务
|
||||
|
||||
如果您的后端系统只支持 RabbitMQ,可以创建一个简单的 HTTP API 服务来桥接 WordPress 和 RabbitMQ。
|
||||
|
||||
示例代码(Go):
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"github.com/streadway/amqp"
|
||||
)
|
||||
|
||||
func createPaymentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req PaymentRequest
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
// 发送到 RabbitMQ
|
||||
conn, _ := amqp.Dial("amqp://guest:guest@localhost:5672/")
|
||||
ch, _ := conn.Channel()
|
||||
|
||||
body, _ := json.Marshal(req)
|
||||
ch.Publish("pay.exchange", "pay.auto.routing.key", false, false, amqp.Publishing{
|
||||
ContentType: "application/json",
|
||||
Body: body,
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/api/payment/create", createPaymentHandler)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
```
|
||||
|
||||
## 数据库结构
|
||||
|
||||
插件会自动创建以下数据库表:
|
||||
|
||||
- `wp_m2pool_eth_payments`: 存储支付记录
|
||||
|
||||
表结构:
|
||||
- `id`: 主键
|
||||
- `order_id`: WooCommerce 订单 ID
|
||||
- `queue_id`: 支付队列 ID
|
||||
- `from_address`: 发送地址
|
||||
- `to_address`: 接收地址
|
||||
- `amount`: 支付金额
|
||||
- `fee`: 手续费
|
||||
- `chain`: 链名称
|
||||
- `symbol`: 代币符号
|
||||
- `tx_hash`: 交易哈希
|
||||
- `block_height`: 区块高度
|
||||
- `status`: 支付状态
|
||||
- `created_at`: 创建时间
|
||||
- `updated_at`: 更新时间
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. 客户选择 ETH 支付方式
|
||||
2. 系统生成支付地址和金额
|
||||
3. 客户向指定地址支付
|
||||
4. 系统自动监听支付状态
|
||||
5. 支付确认后自动更新订单状态
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
m2pool-eth-payment/
|
||||
├── m2pool-eth-payment.php # 主插件文件
|
||||
├── includes/
|
||||
│ ├── class-m2pool-eth-gateway.php # 支付网关类
|
||||
│ ├── class-m2pool-eth-api.php # API 客户端
|
||||
│ └── class-m2pool-eth-listener.php # 支付监听器
|
||||
├── templates/
|
||||
│ ├── settings.php # 设置页面模板
|
||||
│ └── payment-instructions.php # 支付说明模板
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 扩展开发
|
||||
|
||||
要添加新的功能,可以:
|
||||
|
||||
1. 扩展 `M2Pool_ETH_Gateway` 类添加新的支付方式
|
||||
2. 扩展 `M2Pool_ETH_API` 类添加新的 API 接口
|
||||
3. 修改 `M2Pool_ETH_Listener` 类自定义监听逻辑
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 支持
|
||||
|
||||
如有问题,请提交 Issue 或联系开发团队。
|
||||
|
||||
78
integration/wordpress/api-bridge/README.md
Normal file
78
integration/wordpress/api-bridge/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# M2Pool API Bridge
|
||||
|
||||
这是一个中间 API 服务,用于连接 WordPress 插件和基于 RabbitMQ 的支付系统。
|
||||
|
||||
## 功能
|
||||
|
||||
- 接收 WordPress 插件的 HTTP 请求
|
||||
- 将请求转换为 RabbitMQ 消息
|
||||
- 监听 RabbitMQ 响应并返回给 WordPress
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
cd api-bridge
|
||||
go mod download
|
||||
go build -o api-bridge main.go
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
修改 `main.go` 中的 RabbitMQ 配置:
|
||||
|
||||
```go
|
||||
rmqConfig = RabbitMQConfig{
|
||||
URL: "amqp://m2pool:m2pool@localhost:5672",
|
||||
Exchange: "pay.exchange",
|
||||
Queues: map[string]string{
|
||||
"pay": "pay.auto.routing.key",
|
||||
"topup": "pay.recharge.routing.key",
|
||||
"status": "pay.auto.return.routing.key",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
./api-bridge
|
||||
```
|
||||
|
||||
服务将在 `http://localhost:8080` 启动。
|
||||
|
||||
## API 接口
|
||||
|
||||
### 创建支付请求
|
||||
|
||||
```
|
||||
POST /api/payment/create
|
||||
```
|
||||
|
||||
### 查询支付状态
|
||||
|
||||
```
|
||||
GET /api/payment/status/{queue_id}
|
||||
```
|
||||
|
||||
### 创建充值请求
|
||||
|
||||
```
|
||||
POST /api/topup/create
|
||||
```
|
||||
|
||||
### 查询充值状态
|
||||
|
||||
```
|
||||
GET /api/topup/status/{queue_id}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
当前实现是基础版本,实际使用中需要:
|
||||
|
||||
1. 实现状态缓存(Redis 或数据库)
|
||||
2. 监听 RabbitMQ 响应队列
|
||||
3. 实现签名验证
|
||||
4. 添加错误处理和日志记录
|
||||
5. 添加认证和授权
|
||||
|
||||
8
integration/wordpress/api-bridge/go.mod
Normal file
8
integration/wordpress/api-bridge/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module m2pool-api-bridge
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/streadway/amqp v1.1.0
|
||||
)
|
||||
4
integration/wordpress/api-bridge/go.sum
Normal file
4
integration/wordpress/api-bridge/go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM=
|
||||
github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg=
|
||||
241
integration/wordpress/api-bridge/main.go
Normal file
241
integration/wordpress/api-bridge/main.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/streadway/amqp"
|
||||
)
|
||||
|
||||
// PaymentRequest 支付请求
|
||||
type PaymentRequest struct {
|
||||
QueueID string `json:"queue_id"`
|
||||
Chain string `json:"chain"`
|
||||
Symbol string `json:"symbol"`
|
||||
FromAddress string `json:"from_address"`
|
||||
ToAddress string `json:"to_address"`
|
||||
Amount float64 `json:"amount"`
|
||||
Fee float64 `json:"fee"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
// TopupRequest 充值请求
|
||||
type TopupRequest struct {
|
||||
QueueID string `json:"queue_id"`
|
||||
Chain string `json:"chain"`
|
||||
Symbol string `json:"symbol"`
|
||||
Address string `json:"address"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
// APIResponse API 响应
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// RabbitMQConfig RabbitMQ 配置
|
||||
type RabbitMQConfig struct {
|
||||
URL string
|
||||
Exchange string
|
||||
Queues map[string]string
|
||||
}
|
||||
|
||||
var rmqConfig RabbitMQConfig
|
||||
var rmqConn *amqp.Connection
|
||||
var rmqCh *amqp.Channel
|
||||
|
||||
func main() {
|
||||
// 初始化 RabbitMQ 配置
|
||||
rmqConfig = RabbitMQConfig{
|
||||
URL: "amqp://m2pool:m2pool@localhost:5672",
|
||||
Exchange: "pay.exchange",
|
||||
Queues: map[string]string{
|
||||
"pay": "pay.auto.routing.key",
|
||||
"topup": "pay.recharge.routing.key",
|
||||
"status": "pay.auto.return.routing.key",
|
||||
},
|
||||
}
|
||||
|
||||
// 连接 RabbitMQ
|
||||
var err error
|
||||
rmqConn, err = amqp.Dial(rmqConfig.URL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to RabbitMQ: %v", err)
|
||||
}
|
||||
defer rmqConn.Close()
|
||||
|
||||
rmqCh, err = rmqConn.Channel()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open channel: %v", err)
|
||||
}
|
||||
defer rmqCh.Close()
|
||||
|
||||
// 设置路由
|
||||
r := mux.NewRouter()
|
||||
|
||||
// 支付相关接口
|
||||
r.HandleFunc("/api/payment/create", createPaymentHandler).Methods("POST")
|
||||
r.HandleFunc("/api/payment/status/{queue_id}", getPaymentStatusHandler).Methods("GET")
|
||||
|
||||
// 充值相关接口
|
||||
r.HandleFunc("/api/topup/create", createTopupHandler).Methods("POST")
|
||||
r.HandleFunc("/api/topup/status/{queue_id}", getTopupStatusHandler).Methods("GET")
|
||||
|
||||
// 健康检查
|
||||
r.HandleFunc("/health", healthCheckHandler).Methods("GET")
|
||||
|
||||
log.Println("API Bridge Server starting on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8888", r))
|
||||
}
|
||||
|
||||
// createPaymentHandler 创建支付请求
|
||||
func createPaymentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req PaymentRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondJSON(w, http.StatusBadRequest, APIResponse{
|
||||
Success: false,
|
||||
Message: "Invalid request body",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 发送到 RabbitMQ
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
respondJSON(w, http.StatusInternalServerError, APIResponse{
|
||||
Success: false,
|
||||
Message: "Failed to marshal request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = rmqCh.Publish(
|
||||
rmqConfig.Exchange,
|
||||
rmqConfig.Queues["pay"],
|
||||
false,
|
||||
false,
|
||||
amqp.Publishing{
|
||||
ContentType: "application/json",
|
||||
Body: body,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to publish payment request: %v", err)
|
||||
respondJSON(w, http.StatusInternalServerError, APIResponse{
|
||||
Success: false,
|
||||
Message: "Failed to send payment request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, APIResponse{
|
||||
Success: true,
|
||||
Data: map[string]string{
|
||||
"queue_id": req.QueueID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// getPaymentStatusHandler 查询支付状态
|
||||
func getPaymentStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
queueID := vars["queue_id"]
|
||||
|
||||
// 这里应该从数据库或缓存中查询状态
|
||||
// 为了演示,我们返回一个模拟响应
|
||||
// 实际实现中,应该监听 RabbitMQ 的响应队列并存储状态
|
||||
|
||||
respondJSON(w, http.StatusOK, APIResponse{
|
||||
Success: true,
|
||||
Data: map[string]interface{}{
|
||||
"queue_id": queueID,
|
||||
"status": 0, // 0=待支付, 1=成功, 2=待确认, 3=失败
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// createTopupHandler 创建充值请求
|
||||
func createTopupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req TopupRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondJSON(w, http.StatusBadRequest, APIResponse{
|
||||
Success: false,
|
||||
Message: "Invalid request body",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 发送到 RabbitMQ
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
respondJSON(w, http.StatusInternalServerError, APIResponse{
|
||||
Success: false,
|
||||
Message: "Failed to marshal request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = rmqCh.Publish(
|
||||
rmqConfig.Exchange,
|
||||
rmqConfig.Queues["topup"],
|
||||
false,
|
||||
false,
|
||||
amqp.Publishing{
|
||||
ContentType: "application/json",
|
||||
Body: body,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to publish topup request: %v", err)
|
||||
respondJSON(w, http.StatusInternalServerError, APIResponse{
|
||||
Success: false,
|
||||
Message: "Failed to send topup request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, APIResponse{
|
||||
Success: true,
|
||||
Data: map[string]string{
|
||||
"queue_id": req.QueueID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// getTopupStatusHandler 查询充值状态
|
||||
func getTopupStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
queueID := vars["queue_id"]
|
||||
|
||||
// 这里应该从数据库或缓存中查询状态
|
||||
respondJSON(w, http.StatusOK, APIResponse{
|
||||
Success: true,
|
||||
Data: map[string]interface{}{
|
||||
"queue_id": queueID,
|
||||
"status": 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// healthCheckHandler 健康检查
|
||||
func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
respondJSON(w, http.StatusOK, APIResponse{
|
||||
Success: true,
|
||||
Message: "API Bridge is running",
|
||||
})
|
||||
}
|
||||
|
||||
// respondJSON 返回 JSON 响应
|
||||
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
160
integration/wordpress/includes/class-m2pool-eth-api.php
Normal file
160
integration/wordpress/includes/class-m2pool-eth-api.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
/**
|
||||
* M2Pool ETH API 客户端
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class M2Pool_ETH_API {
|
||||
|
||||
/**
|
||||
* API 基础 URL
|
||||
*/
|
||||
private $api_url;
|
||||
|
||||
/**
|
||||
* API 密钥
|
||||
*/
|
||||
private $api_key;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->api_url = get_option('m2pool_eth_api_url', 'http://localhost:8080');
|
||||
$this->api_key = get_option('m2pool_eth_api_key', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*/
|
||||
private function generate_sign($timestamp) {
|
||||
if (empty($this->api_key)) {
|
||||
return '';
|
||||
}
|
||||
return hash('sha256', dechex($timestamp) . $this->api_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求
|
||||
*/
|
||||
private function request($endpoint, $method = 'GET', $data = array()) {
|
||||
$url = rtrim($this->api_url, '/') . '/' . ltrim($endpoint, '/');
|
||||
|
||||
$args = array(
|
||||
'method' => $method,
|
||||
'timeout' => 30,
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
),
|
||||
);
|
||||
|
||||
if (!empty($data)) {
|
||||
$args['body'] = json_encode($data);
|
||||
}
|
||||
|
||||
$response = wp_remote_request($url, $args);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
$code = wp_remote_retrieve_response_code($response);
|
||||
|
||||
if ($code >= 200 && $code < 300) {
|
||||
$data = json_decode($body, true);
|
||||
return $data;
|
||||
} else {
|
||||
return new WP_Error('api_error', sprintf(__('API 错误: %s', 'm2pool-eth-payment'), $body), array('status' => $code));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付请求
|
||||
*
|
||||
* 注意:这个方法假设有一个 HTTP API 接口。
|
||||
* 如果后端系统只支持 RabbitMQ,需要创建一个中间 API 服务。
|
||||
*/
|
||||
public function create_payment($queue_id, $from_address, $to_address, $amount, $chain = 'ETH', $symbol = 'ETH') {
|
||||
$timestamp = time();
|
||||
$sign = $this->generate_sign($timestamp);
|
||||
|
||||
$data = array(
|
||||
'queue_id' => $queue_id,
|
||||
'chain' => $chain,
|
||||
'symbol' => $symbol,
|
||||
'from_address' => $from_address,
|
||||
'to_address' => $to_address,
|
||||
'amount' => $amount,
|
||||
'fee' => 0,
|
||||
'timestamp' => $timestamp,
|
||||
'sign' => $sign,
|
||||
);
|
||||
|
||||
// 发送到支付 API
|
||||
// 注意:这里需要根据实际的后端 API 接口调整
|
||||
// 如果后端只支持 RabbitMQ,需要创建一个中间 API 服务来处理
|
||||
$result = $this->request('/api/payment/create', 'POST', $data);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付状态
|
||||
*/
|
||||
public function get_payment_status($queue_id) {
|
||||
$result = $this->request('/api/payment/status/' . $queue_id, 'GET');
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建充值监听请求
|
||||
*/
|
||||
public function create_topup($queue_id, $address, $chain = 'ETH', $symbol = 'ETH') {
|
||||
$timestamp = time();
|
||||
$sign = $this->generate_sign($timestamp);
|
||||
|
||||
$data = array(
|
||||
'queue_id' => $queue_id,
|
||||
'chain' => $chain,
|
||||
'symbol' => $symbol,
|
||||
'address' => $address,
|
||||
'timestamp' => $timestamp,
|
||||
'sign' => $sign,
|
||||
);
|
||||
|
||||
$result = $this->request('/api/topup/create', 'POST', $data);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询充值状态
|
||||
*/
|
||||
public function get_topup_status($queue_id) {
|
||||
$result = $this->request('/api/topup/status/' . $queue_id, 'GET');
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
320
integration/wordpress/includes/class-m2pool-eth-gateway.php
Normal file
320
integration/wordpress/includes/class-m2pool-eth-gateway.php
Normal file
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
/**
|
||||
* M2Pool ETH 支付网关类
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class M2Pool_ETH_Gateway extends WC_Payment_Gateway {
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->id = 'm2pool_eth';
|
||||
$this->icon = apply_filters('woocommerce_m2pool_eth_icon', '');
|
||||
$this->has_fields = false;
|
||||
$this->method_title = __('M2Pool ETH 支付', 'm2pool-eth-payment');
|
||||
$this->method_description = __('接受以太坊 (ETH) 支付', 'm2pool-eth-payment');
|
||||
$this->supports = array(
|
||||
'products',
|
||||
);
|
||||
|
||||
// 加载设置
|
||||
$this->init_form_fields();
|
||||
$this->init_settings();
|
||||
|
||||
// 定义用户可见的设置
|
||||
$this->title = $this->get_option('title', __('ETH 支付', 'm2pool-eth-payment'));
|
||||
$this->description = $this->get_option('description', __('使用以太坊进行支付', 'm2pool-eth-payment'));
|
||||
$this->enabled = $this->get_option('enabled', 'no');
|
||||
|
||||
// 保存设置
|
||||
add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options'));
|
||||
|
||||
// 支付完成页面
|
||||
add_action('woocommerce_thankyou_' . $this->id, array($this, 'thankyou_page'));
|
||||
|
||||
// 订单详情页面
|
||||
add_action('woocommerce_order_details_after_order_table', array($this, 'order_details'), 10, 1);
|
||||
|
||||
// 确保在结账页面显示
|
||||
add_filter('woocommerce_available_payment_gateways', array($this, 'ensure_available'), 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保支付网关可用(强制显示)
|
||||
*/
|
||||
public function ensure_available($available_gateways) {
|
||||
// 如果已启用且配置正确,确保在列表中
|
||||
if ($this->enabled === 'yes') {
|
||||
$to_address = $this->get_option('to_address', get_option('m2pool_eth_to_address', ''));
|
||||
if (!empty($to_address) && $this->is_available()) {
|
||||
// 确保网关在可用列表中
|
||||
if (!isset($available_gateways[$this->id])) {
|
||||
$available_gateways[$this->id] = $this;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $available_gateways;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查支付网关是否可用
|
||||
*/
|
||||
public function is_available() {
|
||||
// 检查是否启用
|
||||
if ($this->enabled !== 'yes') {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('M2Pool ETH Gateway: Not enabled');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有接收地址
|
||||
$to_address = $this->get_option('to_address', get_option('m2pool_eth_to_address', ''));
|
||||
if (empty($to_address)) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('M2Pool ETH Gateway: No receive address configured');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有订单(在结账页面)
|
||||
if (is_checkout() || is_checkout_pay_page()) {
|
||||
// 在结账页面,确保有订单
|
||||
$order_id = absint(get_query_var('order-pay'));
|
||||
if ($order_id > 0) {
|
||||
$order = wc_get_order($order_id);
|
||||
if (!$order || $order->get_total() <= 0) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('M2Pool ETH Gateway: Invalid order or zero amount');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 调用父类方法
|
||||
$parent_available = parent::is_available();
|
||||
|
||||
// 如果父类返回 false,记录原因(用于调试)
|
||||
if (!$parent_available && defined('WP_DEBUG') && WP_DEBUG) {
|
||||
$order = WC()->cart;
|
||||
$total = $order ? $order->get_total() : 0;
|
||||
error_log(sprintf(
|
||||
'M2Pool ETH Gateway: Parent is_available() returned false. Cart total: %s, Enabled: %s, Address: %s',
|
||||
$total,
|
||||
$this->enabled,
|
||||
!empty($to_address) ? 'set' : 'empty'
|
||||
));
|
||||
}
|
||||
|
||||
return $parent_available;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单字段
|
||||
*/
|
||||
public function init_form_fields() {
|
||||
$this->form_fields = array(
|
||||
'enabled' => array(
|
||||
'title' => __('启用/禁用', 'm2pool-eth-payment'),
|
||||
'type' => 'checkbox',
|
||||
'label' => __('启用 M2Pool ETH 支付', 'm2pool-eth-payment'),
|
||||
'default' => 'no',
|
||||
),
|
||||
'title' => array(
|
||||
'title' => __('标题', 'm2pool-eth-payment'),
|
||||
'type' => 'text',
|
||||
'description' => __('客户在结账时看到的支付方式标题', 'm2pool-eth-payment'),
|
||||
'default' => __('ETH 支付', 'm2pool-eth-payment'),
|
||||
'desc_tip' => true,
|
||||
),
|
||||
'description' => array(
|
||||
'title' => __('描述', 'm2pool-eth-payment'),
|
||||
'type' => 'textarea',
|
||||
'description' => __('客户在结账时看到的支付方式描述', 'm2pool-eth-payment'),
|
||||
'default' => __('使用以太坊 (ETH) 进行支付', 'm2pool-eth-payment'),
|
||||
'desc_tip' => true,
|
||||
),
|
||||
'from_address' => array(
|
||||
'title' => __('发送地址', 'm2pool-eth-payment'),
|
||||
'type' => 'text',
|
||||
'description' => __('用于接收支付的以太坊地址', 'm2pool-eth-payment'),
|
||||
'default' => get_option('m2pool_eth_from_address', ''),
|
||||
'desc_tip' => true,
|
||||
),
|
||||
'to_address' => array(
|
||||
'title' => __('接收地址', 'm2pool-eth-payment'),
|
||||
'type' => 'text',
|
||||
'description' => __('用于接收支付的以太坊地址', 'm2pool-eth-payment'),
|
||||
'default' => get_option('m2pool_eth_to_address', ''),
|
||||
'desc_tip' => true,
|
||||
),
|
||||
'chain' => array(
|
||||
'title' => __('链名称', 'm2pool-eth-payment'),
|
||||
'type' => 'select',
|
||||
'options' => array(
|
||||
'ETH' => 'Ethereum',
|
||||
),
|
||||
'default' => 'ETH',
|
||||
),
|
||||
'symbol' => array(
|
||||
'title' => __('代币符号', 'm2pool-eth-payment'),
|
||||
'type' => 'select',
|
||||
'options' => array(
|
||||
'ETH' => 'ETH',
|
||||
'USDT' => 'USDT',
|
||||
),
|
||||
'default' => 'ETH',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付
|
||||
*/
|
||||
public function process_payment($order_id) {
|
||||
$order = wc_get_order($order_id);
|
||||
|
||||
if (!$order) {
|
||||
wc_add_notice(__('订单不存在', 'm2pool-eth-payment'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取 API 客户端
|
||||
$api = new M2Pool_ETH_API();
|
||||
|
||||
// 准备支付请求
|
||||
$amount = floatval($order->get_total());
|
||||
$from_address = $this->get_option('from_address', get_option('m2pool_eth_from_address', ''));
|
||||
$to_address = $this->get_option('to_address', get_option('m2pool_eth_to_address', ''));
|
||||
$chain = $this->get_option('chain', 'ETH');
|
||||
$symbol = $this->get_option('symbol', 'ETH');
|
||||
|
||||
if (empty($to_address)) {
|
||||
wc_add_notice(__('支付网关配置错误:接收地址未设置', 'm2pool-eth-payment'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成队列 ID
|
||||
$queue_id = 'wp_' . $order_id . '_' . time();
|
||||
|
||||
// 发送支付请求
|
||||
$result = $api->create_payment($queue_id, $from_address, $to_address, $amount, $chain, $symbol);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
wc_add_notice($result->get_error_message(), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存支付信息到数据库
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
|
||||
|
||||
$wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'order_id' => $order_id,
|
||||
'queue_id' => $queue_id,
|
||||
'from_address' => $from_address,
|
||||
'to_address' => $to_address,
|
||||
'amount' => $amount,
|
||||
'fee' => isset($result['fee']) ? $result['fee'] : 0,
|
||||
'chain' => $chain,
|
||||
'symbol' => $symbol,
|
||||
'status' => 0, // 待支付
|
||||
),
|
||||
array('%d', '%s', '%s', '%s', '%f', '%f', '%s', '%s', '%d')
|
||||
);
|
||||
|
||||
// 更新订单元数据
|
||||
$order->update_meta_data('_m2pool_queue_id', $queue_id);
|
||||
$order->update_meta_data('_m2pool_payment_address', $to_address);
|
||||
$order->update_meta_data('_m2pool_payment_amount', $amount);
|
||||
$order->update_meta_data('_m2pool_payment_chain', $chain);
|
||||
$order->update_meta_data('_m2pool_payment_symbol', $symbol);
|
||||
$order->save();
|
||||
|
||||
// 将订单状态设置为待支付
|
||||
$order->update_status('pending', __('等待 ETH 支付', 'm2pool-eth-payment'));
|
||||
|
||||
// 启动支付监听
|
||||
$listener = new M2Pool_ETH_Listener();
|
||||
$listener->start_listening($order_id);
|
||||
|
||||
// 返回成功
|
||||
return array(
|
||||
'result' => 'success',
|
||||
'redirect' => $this->get_return_url($order),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 感谢页面
|
||||
*/
|
||||
public function thankyou_page($order_id) {
|
||||
$order = wc_get_order($order_id);
|
||||
|
||||
if (!$order || $order->get_payment_method() !== $this->id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment_address = $order->get_meta('_m2pool_payment_address');
|
||||
$payment_amount = $order->get_meta('_m2pool_payment_amount');
|
||||
$payment_symbol = $order->get_meta('_m2pool_payment_symbol');
|
||||
$queue_id = $order->get_meta('_m2pool_queue_id');
|
||||
|
||||
if (!$payment_address || !$payment_amount) {
|
||||
return;
|
||||
}
|
||||
|
||||
include M2POOL_ETH_PLUGIN_DIR . 'templates/payment-instructions.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单详情页面
|
||||
*/
|
||||
public function order_details($order) {
|
||||
if ($order->get_payment_method() !== $this->id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment_address = $order->get_meta('_m2pool_payment_address');
|
||||
$payment_amount = $order->get_meta('_m2pool_payment_amount');
|
||||
$payment_symbol = $order->get_meta('_m2pool_payment_symbol');
|
||||
$tx_hash = $order->get_meta('_m2pool_tx_hash');
|
||||
|
||||
if (!$payment_address || !$payment_amount) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<section class="woocommerce-order-details m2pool-eth-payment-details">
|
||||
<h2 class="woocommerce-order-details__title"><?php echo esc_html__('ETH 支付信息', 'm2pool-eth-payment'); ?></h2>
|
||||
<table class="woocommerce-table woocommerce-table--order-details shop_table order_details">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><?php echo esc_html__('支付地址', 'm2pool-eth-payment'); ?>:</th>
|
||||
<td><code><?php echo esc_html($payment_address); ?></code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php echo esc_html__('支付金额', 'm2pool-eth-payment'); ?>:</th>
|
||||
<td><?php echo esc_html($payment_amount); ?> <?php echo esc_html($payment_symbol); ?></td>
|
||||
</tr>
|
||||
<?php if ($tx_hash): ?>
|
||||
<tr>
|
||||
<th><?php echo esc_html__('交易哈希', 'm2pool-eth-payment'); ?>:</th>
|
||||
<td><a href="https://etherscan.io/tx/<?php echo esc_attr($tx_hash); ?>" target="_blank"><?php echo esc_html($tx_hash); ?></a></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
255
integration/wordpress/includes/class-m2pool-eth-listener.php
Normal file
255
integration/wordpress/includes/class-m2pool-eth-listener.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
/**
|
||||
* M2Pool ETH 支付监听器
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class M2Pool_ETH_Listener {
|
||||
|
||||
/**
|
||||
* API 客户端
|
||||
*/
|
||||
private $api;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->api = new M2Pool_ETH_API();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始监听订单支付
|
||||
*/
|
||||
public function start_listening($order_id) {
|
||||
// 创建定时任务检查支付状态
|
||||
if (!wp_next_scheduled('m2pool_eth_check_payments')) {
|
||||
$interval = get_option('m2pool_eth_listen_interval', 30);
|
||||
wp_schedule_event(time(), 'm2pool_eth_interval', 'm2pool_eth_check_payments');
|
||||
}
|
||||
|
||||
// 添加自定义间隔
|
||||
add_filter('cron_schedules', array($this, 'add_cron_interval'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义 Cron 间隔
|
||||
*/
|
||||
public function add_cron_interval($schedules) {
|
||||
$interval = get_option('m2pool_eth_listen_interval', 30);
|
||||
|
||||
$schedules['m2pool_eth_interval'] = array(
|
||||
'interval' => $interval,
|
||||
'display' => sprintf(__('每 %d 秒', 'm2pool-eth-payment'), $interval),
|
||||
);
|
||||
|
||||
return $schedules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查支付状态
|
||||
*/
|
||||
public function check_payment_status($order_id) {
|
||||
$order = wc_get_order($order_id);
|
||||
|
||||
if (!$order) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __('订单不存在', 'm2pool-eth-payment'),
|
||||
);
|
||||
}
|
||||
|
||||
$queue_id = $order->get_meta('_m2pool_queue_id');
|
||||
|
||||
if (empty($queue_id)) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __('未找到支付队列 ID', 'm2pool-eth-payment'),
|
||||
);
|
||||
}
|
||||
|
||||
// 从数据库获取支付记录
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
|
||||
$payment = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE order_id = %d AND queue_id = %s",
|
||||
$order_id,
|
||||
$queue_id
|
||||
));
|
||||
|
||||
if (!$payment) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => __('未找到支付记录', 'm2pool-eth-payment'),
|
||||
);
|
||||
}
|
||||
|
||||
// 如果已经成功,直接返回
|
||||
if ($payment->status == 1) {
|
||||
return array(
|
||||
'success' => true,
|
||||
'status' => 'completed',
|
||||
'message' => __('支付已完成', 'm2pool-eth-payment'),
|
||||
);
|
||||
}
|
||||
|
||||
// 查询 API 获取最新状态
|
||||
$result = $this->api->get_payment_status($queue_id);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'message' => $result->get_error_message(),
|
||||
);
|
||||
}
|
||||
|
||||
// 更新支付状态
|
||||
$this->update_payment_status($order_id, $result);
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'status' => $this->get_status_text($result['status']),
|
||||
'data' => $result,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Webhook
|
||||
*/
|
||||
public function process_webhook($data) {
|
||||
if (!isset($data['queue_id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$queue_id = $data['queue_id'];
|
||||
|
||||
// 从数据库查找订单
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
|
||||
$payment = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE queue_id = %s",
|
||||
$queue_id
|
||||
));
|
||||
|
||||
if (!$payment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新支付状态
|
||||
$this->update_payment_status($payment->order_id, $data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付状态
|
||||
*/
|
||||
private function update_payment_status($order_id, $data) {
|
||||
$order = wc_get_order($order_id);
|
||||
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
|
||||
|
||||
$status = isset($data['status']) ? intval($data['status']) : 0;
|
||||
$tx_hash = isset($data['tx_hash']) ? $data['tx_hash'] : null;
|
||||
$block_height = isset($data['block_height']) ? $data['block_height'] : null;
|
||||
$amount = isset($data['amount']) ? floatval($data['amount']) : null;
|
||||
|
||||
// 更新数据库
|
||||
$update_data = array(
|
||||
'status' => $status,
|
||||
);
|
||||
|
||||
if ($tx_hash) {
|
||||
$update_data['tx_hash'] = $tx_hash;
|
||||
}
|
||||
|
||||
if ($block_height) {
|
||||
$update_data['block_height'] = $block_height;
|
||||
}
|
||||
|
||||
if ($amount !== null) {
|
||||
$update_data['amount'] = $amount;
|
||||
}
|
||||
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
$update_data,
|
||||
array('order_id' => $order_id),
|
||||
array('%d', '%s', '%d', '%f'),
|
||||
array('%d')
|
||||
);
|
||||
|
||||
// 更新订单状态
|
||||
if ($status == 1) {
|
||||
// 支付成功
|
||||
$order->update_meta_data('_m2pool_tx_hash', $tx_hash);
|
||||
$order->payment_complete();
|
||||
$order->add_order_note(__('ETH 支付成功', 'm2pool-eth-payment'));
|
||||
} elseif ($status == 2) {
|
||||
// 待确认
|
||||
$order->update_status('on-hold', __('ETH 支付待确认', 'm2pool-eth-payment'));
|
||||
if ($tx_hash) {
|
||||
$order->update_meta_data('_m2pool_tx_hash', $tx_hash);
|
||||
$order->add_order_note(sprintf(__('ETH 支付待确认,交易哈希: %s', 'm2pool-eth-payment'), $tx_hash));
|
||||
}
|
||||
} elseif ($status == 0 || $status == 3) {
|
||||
// 支付失败
|
||||
$order->update_status('failed', __('ETH 支付失败', 'm2pool-eth-payment'));
|
||||
$order->add_order_note(__('ETH 支付失败', 'm2pool-eth-payment'));
|
||||
}
|
||||
|
||||
$order->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
private function get_status_text($status) {
|
||||
$statuses = array(
|
||||
0 => 'pending',
|
||||
1 => 'completed',
|
||||
2 => 'processing',
|
||||
3 => 'failed',
|
||||
);
|
||||
|
||||
return isset($statuses[$status]) ? $statuses[$status] : 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量检查所有待支付订单
|
||||
*/
|
||||
public function check_all_pending_payments() {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
|
||||
|
||||
// 获取所有待支付和待确认的订单
|
||||
$payments = $wpdb->get_results(
|
||||
"SELECT * FROM $table_name WHERE status IN (0, 2) ORDER BY created_at ASC LIMIT 50"
|
||||
);
|
||||
|
||||
foreach ($payments as $payment) {
|
||||
$this->check_payment_status($payment->order_id);
|
||||
|
||||
// 避免请求过快
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 注册定时任务处理函数
|
||||
add_action('m2pool_eth_check_payments', 'm2pool_eth_check_all_payments');
|
||||
|
||||
// 定时任务处理函数
|
||||
function m2pool_eth_check_all_payments() {
|
||||
$listener = new M2Pool_ETH_Listener();
|
||||
$listener->check_all_pending_payments();
|
||||
}
|
||||
|
||||
275
integration/wordpress/m2pool-eth-payment.php
Normal file
275
integration/wordpress/m2pool-eth-payment.php
Normal file
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: M2Pool ETH Payment Gateway
|
||||
* Plugin URI: https://github.com/m2pool/payment-gateway
|
||||
* Description: 支持 ETH 交易的 WordPress 支付网关,支持支付、监听和返回支付结果
|
||||
* Version: 1.0.0
|
||||
* Author: M2Pool
|
||||
* Author URI: https://m2pool.com
|
||||
* Text Domain: m2pool-eth-payment
|
||||
* Domain Path: /languages
|
||||
* Requires at least: 5.0
|
||||
* Requires PHP: 7.4
|
||||
* WC requires at least: 3.0
|
||||
* WC tested up to: 8.0
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit; // Exit if accessed directly
|
||||
}
|
||||
|
||||
// 定义插件常量
|
||||
define('M2POOL_ETH_VERSION', '1.0.0');
|
||||
define('M2POOL_ETH_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('M2POOL_ETH_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
define('M2POOL_ETH_PLUGIN_FILE', __FILE__);
|
||||
|
||||
/**
|
||||
* 主插件类
|
||||
*/
|
||||
class M2Pool_ETH_Payment {
|
||||
|
||||
/**
|
||||
* 单例实例
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks() {
|
||||
// 激活插件时创建数据库表
|
||||
register_activation_hook(__FILE__, array($this, 'activate'));
|
||||
|
||||
// 停用插件时清理
|
||||
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
|
||||
|
||||
// 在 WooCommerce 加载后初始化
|
||||
add_action('plugins_loaded', array($this, 'init'), 11);
|
||||
|
||||
// 添加管理菜单
|
||||
add_action('admin_menu', array($this, 'add_admin_menu'));
|
||||
|
||||
// 注册 AJAX 处理
|
||||
add_action('wp_ajax_m2pool_check_payment', array($this, 'ajax_check_payment'));
|
||||
add_action('wp_ajax_nopriv_m2pool_check_payment', array($this, 'ajax_check_payment'));
|
||||
|
||||
// 注册 Webhook 端点
|
||||
add_action('rest_api_init', array($this, 'register_webhook_routes'));
|
||||
|
||||
// 加载文本域
|
||||
add_action('init', array($this, 'load_textdomain'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化插件
|
||||
*/
|
||||
public function init() {
|
||||
// 检查 WooCommerce 是否激活
|
||||
if (!class_exists('WooCommerce')) {
|
||||
add_action('admin_notices', array($this, 'woocommerce_missing_notice'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载支付网关类
|
||||
require_once M2POOL_ETH_PLUGIN_DIR . 'includes/class-m2pool-eth-gateway.php';
|
||||
require_once M2POOL_ETH_PLUGIN_DIR . 'includes/class-m2pool-eth-api.php';
|
||||
require_once M2POOL_ETH_PLUGIN_DIR . 'includes/class-m2pool-eth-listener.php';
|
||||
|
||||
// 注册支付网关(必须在类加载后)
|
||||
add_filter('woocommerce_payment_gateways', array($this, 'add_gateway'));
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce 缺失通知
|
||||
*/
|
||||
public function woocommerce_missing_notice() {
|
||||
?>
|
||||
<div class="error">
|
||||
<p><?php echo esc_html__('M2Pool ETH Payment Gateway 需要 WooCommerce 插件才能工作。', 'm2pool-eth-payment'); ?></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加支付网关
|
||||
*/
|
||||
public function add_gateway($gateways) {
|
||||
$gateways[] = 'M2Pool_ETH_Gateway';
|
||||
return $gateways;
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活插件
|
||||
*/
|
||||
public function activate() {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
$table_name = $wpdb->prefix . 'm2pool_eth_payments';
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
|
||||
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
order_id bigint(20) UNSIGNED NOT NULL,
|
||||
queue_id varchar(255) NOT NULL,
|
||||
from_address varchar(255) NOT NULL,
|
||||
to_address varchar(255) NOT NULL,
|
||||
amount decimal(20,8) NOT NULL,
|
||||
fee decimal(20,8) DEFAULT 0,
|
||||
chain varchar(50) DEFAULT 'ETH',
|
||||
symbol varchar(50) DEFAULT 'ETH',
|
||||
tx_hash varchar(255) DEFAULT NULL,
|
||||
block_height bigint(20) UNSIGNED DEFAULT NULL,
|
||||
status int(11) DEFAULT 0 COMMENT '0=待支付,1=成功,2=待确认,3=失败',
|
||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY queue_id (queue_id),
|
||||
KEY order_id (order_id),
|
||||
KEY tx_hash (tx_hash),
|
||||
KEY status (status)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
dbDelta($sql);
|
||||
|
||||
// 设置默认选项
|
||||
if (!get_option('m2pool_eth_api_url')) {
|
||||
update_option('m2pool_eth_api_url', 'http://localhost:8080');
|
||||
}
|
||||
if (!get_option('m2pool_eth_api_key')) {
|
||||
update_option('m2pool_eth_api_key', '');
|
||||
}
|
||||
if (!get_option('m2pool_eth_listen_interval')) {
|
||||
update_option('m2pool_eth_listen_interval', 30); // 30秒轮询一次
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用插件
|
||||
*/
|
||||
public function deactivate() {
|
||||
// 清理定时任务
|
||||
wp_clear_scheduled_hook('m2pool_eth_check_payments');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加管理菜单
|
||||
*/
|
||||
public function add_admin_menu() {
|
||||
add_options_page(
|
||||
__('M2Pool ETH 支付设置', 'm2pool-eth-payment'),
|
||||
__('M2Pool ETH 支付', 'm2pool-eth-payment'),
|
||||
'manage_options',
|
||||
'm2pool-eth-settings',
|
||||
array($this, 'settings_page')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面
|
||||
*/
|
||||
public function settings_page() {
|
||||
if (isset($_POST['m2pool_eth_save_settings'])) {
|
||||
check_admin_referer('m2pool_eth_settings');
|
||||
|
||||
update_option('m2pool_eth_api_url', sanitize_text_field($_POST['api_url']));
|
||||
update_option('m2pool_eth_api_key', sanitize_text_field($_POST['api_key']));
|
||||
update_option('m2pool_eth_listen_interval', intval($_POST['listen_interval']));
|
||||
update_option('m2pool_eth_from_address', sanitize_text_field($_POST['from_address']));
|
||||
update_option('m2pool_eth_to_address', sanitize_text_field($_POST['to_address']));
|
||||
|
||||
echo '<div class="notice notice-success"><p>' . esc_html__('设置已保存', 'm2pool-eth-payment') . '</p></div>';
|
||||
}
|
||||
|
||||
$api_url = get_option('m2pool_eth_api_url', 'http://localhost:8080');
|
||||
$api_key = get_option('m2pool_eth_api_key', '');
|
||||
$listen_interval = get_option('m2pool_eth_listen_interval', 30);
|
||||
$from_address = get_option('m2pool_eth_from_address', '');
|
||||
$to_address = get_option('m2pool_eth_to_address', '');
|
||||
|
||||
include M2POOL_ETH_PLUGIN_DIR . 'templates/settings.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX 检查支付状态
|
||||
*/
|
||||
public function ajax_check_payment() {
|
||||
check_ajax_referer('m2pool_eth_check', 'nonce');
|
||||
|
||||
$order_id = intval($_POST['order_id']);
|
||||
$order = wc_get_order($order_id);
|
||||
|
||||
if (!$order) {
|
||||
wp_send_json_error(array('message' => __('订单不存在', 'm2pool-eth-payment')));
|
||||
}
|
||||
|
||||
$listener = new M2Pool_ETH_Listener();
|
||||
$result = $listener->check_payment_status($order_id);
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success($result);
|
||||
} else {
|
||||
wp_send_json_error($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 Webhook 路由
|
||||
*/
|
||||
public function register_webhook_routes() {
|
||||
register_rest_route('m2pool-eth/v1', '/webhook', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array($this, 'handle_webhook'),
|
||||
'permission_callback' => '__return_true',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Webhook
|
||||
*/
|
||||
public function handle_webhook($request) {
|
||||
$data = $request->get_json_params();
|
||||
|
||||
if (!isset($data['queue_id'])) {
|
||||
return new WP_Error('invalid_data', __('无效的数据', 'm2pool-eth-payment'), array('status' => 400));
|
||||
}
|
||||
|
||||
$listener = new M2Pool_ETH_Listener();
|
||||
$result = $listener->process_webhook($data);
|
||||
|
||||
if ($result) {
|
||||
return new WP_REST_Response(array('success' => true), 200);
|
||||
} else {
|
||||
return new WP_Error('processing_failed', __('处理失败', 'm2pool-eth-payment'), array('status' => 500));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载文本域
|
||||
*/
|
||||
public function load_textdomain() {
|
||||
load_plugin_textdomain('m2pool-eth-payment', false, dirname(plugin_basename(__FILE__)) . '/languages');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化插件
|
||||
M2Pool_ETH_Payment::get_instance();
|
||||
|
||||
136
integration/wordpress/templates/payment-instructions.php
Normal file
136
integration/wordpress/templates/payment-instructions.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* 支付说明模板
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="m2pool-eth-payment-instructions">
|
||||
<h3><?php echo esc_html__('ETH 支付说明', 'm2pool-eth-payment'); ?></h3>
|
||||
|
||||
<div class="payment-info">
|
||||
<p><strong><?php echo esc_html__('请向以下地址支付:', 'm2pool-eth-payment'); ?></strong></p>
|
||||
<div class="payment-address">
|
||||
<code id="payment-address-code"><?php echo esc_html($payment_address); ?></code>
|
||||
<button type="button" class="button copy-address" data-address="<?php echo esc_attr($payment_address); ?>">
|
||||
<?php echo esc_html__('复制地址', 'm2pool-eth-payment'); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p><strong><?php echo esc_html__('支付金额:', 'm2pool-eth-payment'); ?></strong></p>
|
||||
<p class="payment-amount"><?php echo esc_html($payment_amount); ?> <?php echo esc_html($payment_symbol); ?></p>
|
||||
|
||||
<div class="payment-status" id="payment-status">
|
||||
<p><?php echo esc_html__('等待支付中...', 'm2pool-eth-payment'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-notes">
|
||||
<p><strong><?php echo esc_html__('注意事项:', 'm2pool-eth-payment'); ?></strong></p>
|
||||
<ul>
|
||||
<li><?php echo esc_html__('请确保支付金额准确', 'm2pool-eth-payment'); ?></li>
|
||||
<li><?php echo esc_html__('支付完成后,系统会自动确认(通常需要几分钟)', 'm2pool-eth-payment'); ?></li>
|
||||
<li><?php echo esc_html__('请勿向此地址发送其他代币', 'm2pool-eth-payment'); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function($) {
|
||||
// 复制地址功能
|
||||
$('.copy-address').on('click', function() {
|
||||
var address = $(this).data('address');
|
||||
var $temp = $('<input>');
|
||||
$('body').append($temp);
|
||||
$temp.val(address).select();
|
||||
document.execCommand('copy');
|
||||
$temp.remove();
|
||||
|
||||
$(this).text('<?php echo esc_js(__('已复制', 'm2pool-eth-payment')); ?>');
|
||||
setTimeout(function() {
|
||||
$('.copy-address').text('<?php echo esc_js(__('复制地址', 'm2pool-eth-payment')); ?>');
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// 定期检查支付状态
|
||||
var orderId = <?php echo esc_js($order_id); ?>;
|
||||
var checkInterval = setInterval(function() {
|
||||
$.ajax({
|
||||
url: '<?php echo esc_url(admin_url('admin-ajax.php')); ?>',
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'm2pool_check_payment',
|
||||
order_id: orderId,
|
||||
nonce: '<?php echo esc_js(wp_create_nonce('m2pool_eth_check')); ?>'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.data.status === 'completed') {
|
||||
clearInterval(checkInterval);
|
||||
$('#payment-status').html('<p style="color: green;"><?php echo esc_js(__('支付成功!页面将自动刷新...', 'm2pool-eth-payment')); ?></p>');
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
} else if (response.success && response.data.status === 'processing') {
|
||||
$('#payment-status').html('<p style="color: orange;"><?php echo esc_js(__('支付确认中...', 'm2pool-eth-payment')); ?></p>');
|
||||
} else if (response.success && response.data.status === 'failed') {
|
||||
clearInterval(checkInterval);
|
||||
$('#payment-status').html('<p style="color: red;"><?php echo esc_js(__('支付失败', 'm2pool-eth-payment')); ?></p>');
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 10000); // 每10秒检查一次
|
||||
|
||||
// 30秒后停止检查(避免无限检查)
|
||||
setTimeout(function() {
|
||||
clearInterval(checkInterval);
|
||||
}, 300000); // 5分钟
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.m2pool-eth-payment-instructions {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.payment-address {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.payment-address code {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.payment-amount {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
color: #0073aa;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.payment-status {
|
||||
margin: 15px 0;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.payment-notes ul {
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
81
integration/wordpress/templates/settings.php
Normal file
81
integration/wordpress/templates/settings.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* 设置页面模板
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html__('M2Pool ETH 支付设置', 'm2pool-eth-payment'); ?></h1>
|
||||
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('m2pool_eth_settings'); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="api_url"><?php echo esc_html__('API 地址', 'm2pool-eth-payment'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" id="api_url" name="api_url" value="<?php echo esc_attr($api_url); ?>" class="regular-text" />
|
||||
<p class="description"><?php echo esc_html__('支付系统的 API 地址,例如: http://localhost:8080', 'm2pool-eth-payment'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="api_key"><?php echo esc_html__('API 密钥', 'm2pool-eth-payment'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="api_key" name="api_key" value="<?php echo esc_attr($api_key); ?>" class="regular-text" />
|
||||
<p class="description"><?php echo esc_html__('用于签名验证的 API 密钥', 'm2pool-eth-payment'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="from_address"><?php echo esc_html__('发送地址', 'm2pool-eth-payment'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="from_address" name="from_address" value="<?php echo esc_attr($from_address); ?>" class="regular-text" />
|
||||
<p class="description"><?php echo esc_html__('用于发送支付的以太坊地址(可选)', 'm2pool-eth-payment'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="to_address"><?php echo esc_html__('接收地址', 'm2pool-eth-payment'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="to_address" name="to_address" value="<?php echo esc_attr($to_address); ?>" class="regular-text" />
|
||||
<p class="description"><?php echo esc_html__('用于接收支付的以太坊地址', 'm2pool-eth-payment'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="listen_interval"><?php echo esc_html__('监听间隔(秒)', 'm2pool-eth-payment'); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" id="listen_interval" name="listen_interval" value="<?php echo esc_attr($listen_interval); ?>" min="10" max="300" />
|
||||
<p class="description"><?php echo esc_html__('检查支付状态的间隔时间(秒),建议 30-60 秒', 'm2pool-eth-payment'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<input type="submit" name="m2pool_eth_save_settings" class="button button-primary" value="<?php echo esc_attr__('保存设置', 'm2pool-eth-payment'); ?>" />
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2><?php echo esc_html__('Webhook 设置', 'm2pool-eth-payment'); ?></h2>
|
||||
<p><?php echo esc_html__('Webhook URL(用于接收支付状态通知):', 'm2pool-eth-payment'); ?></p>
|
||||
<code><?php echo esc_url(rest_url('m2pool-eth/v1/webhook')); ?></code>
|
||||
<p class="description"><?php echo esc_html__('将此 URL 配置到您的支付系统中,以便接收支付状态更新', 'm2pool-eth-payment'); ?></p>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
// 区块链网络抽象接口统一实现
|
||||
package blockchain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type IChainServer interface {
|
||||
AddAddress(address string, msg any)
|
||||
RemoveAddress(address string)
|
||||
Listen(symbol string, ch chan any)
|
||||
Transfer(symbol string, msg any) error
|
||||
Listen() error
|
||||
ListenMsg()
|
||||
Transfer(from, to, symbol string, amount, fee float64) error
|
||||
Stop()
|
||||
}
|
||||
|
||||
type BlockChainServer struct {
|
||||
mu sync.Mutex
|
||||
chains map[string]IChainServer // "ETH", "TRON", "BTC"
|
||||
chains map[string]IChainServer
|
||||
}
|
||||
|
||||
func NewBlockChainServer() *BlockChainServer {
|
||||
@@ -30,38 +31,41 @@ func (b *BlockChainServer) RegisterChain(name string, chain IChainServer) {
|
||||
b.chains[name] = chain
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) AddAddress(chain, address string, msg any) {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
srv.AddAddress(address, msg)
|
||||
fmt.Printf("✅ 添加监听地址: chain=%s, address=%s\n", chain, address)
|
||||
} else {
|
||||
fmt.Printf("⚠️ 链未注册: %s\n", chain)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) RemoveAddress(chain, address string) {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
srv.RemoveAddress(address)
|
||||
fmt.Printf("🗑️ 移除监听地址: chain=%s, address=%s\n", chain, address)
|
||||
} else {
|
||||
fmt.Printf("⚠️ 链未注册: %s\n", chain)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) Listen(chain, symbol string, ch chan any) error {
|
||||
func (b *BlockChainServer) Listen(chain string, ch chan any) error {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
go func() {
|
||||
srv.Listen(symbol, ch)
|
||||
err := srv.Listen()
|
||||
if err != nil {
|
||||
log.Printf("Start Listen error: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("链未注册: %s", chain)
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) Transfer(chain, symbol string, msg any) error {
|
||||
func (b *BlockChainServer) ListenMsg(chain string) error {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
fmt.Printf("💸 %s-%s发起转账: %+v\n", chain, symbol, msg)
|
||||
return srv.Transfer(symbol, msg)
|
||||
go func() {
|
||||
srv.ListenMsg()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("链未注册: %s", chain)
|
||||
}
|
||||
|
||||
func (b *BlockChainServer) Transfer(chain, from, to, symbol string, amount, fee float64) error {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
fmt.Printf("💸 发起转账: Chain(%s)-Symbol(%s), From(%s), To(%s), Amount(%f)\n", chain, symbol, from, to, amount)
|
||||
go func() {
|
||||
err := srv.Transfer(from, to, symbol, amount, fee)
|
||||
if err != nil {
|
||||
log.Printf("Transfer Error: %v\n Transfer has Exited", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("链未注册: %s", chain)
|
||||
}
|
||||
@@ -70,6 +74,7 @@ func (b *BlockChainServer) Stop(chain string) {
|
||||
if srv, ok := b.chains[chain]; ok {
|
||||
fmt.Printf("💸 关闭服务: %+v\n", chain)
|
||||
srv.Stop()
|
||||
return
|
||||
}
|
||||
fmt.Printf("链未注册: %s", chain)
|
||||
}
|
||||
|
||||
@@ -5,686 +5,227 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"m2pool-payment/internal/db"
|
||||
"m2pool-payment/internal/listen"
|
||||
message "m2pool-payment/internal/msg"
|
||||
"m2pool-payment/internal/utils"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
||||
const erc20ABI = `
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{"name": "_owner", "type": "address"}],
|
||||
"name": "balanceOf",
|
||||
"outputs": [{"name": "balance", "type": "uint256"}],
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{"name": "_to", "type": "address"},
|
||||
{"name": "_value", "type": "uint256"}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [{"name": "", "type": "bool"}],
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{"indexed": true, "name": "from", "type": "address"},
|
||||
{"indexed": true, "name": "to", "type": "address"},
|
||||
{"indexed": false,"name": "value","type": "uint256"}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
type ETHNode struct {
|
||||
decodeKey string // 私钥解密密钥,从程序启动命令行获得
|
||||
NetId *big.Int // 网络ID,主网为1,其他ID可通过rpc.NetworkID方法获取
|
||||
Config message.ETHConfig // 配置文件
|
||||
WsClient *ethclient.Client
|
||||
RpcClient *ethclient.Client
|
||||
Db db.MySQLPool
|
||||
mu sync.Mutex
|
||||
ListenAddresses sync.Map // key:"钱包地址", value:bool
|
||||
UnConfirmTxs map[string]message.Tx_msg // 待交易地址池,key:"交易hash", value: message.Tx
|
||||
USDT *USDT // ETH相关
|
||||
RmqMsgs map[string][]any // 根据地址查找出该地址涉及的消息,消息需要断言(topupreq_msg, payreq_msg, withdrawreq_msg)
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
decodeKey string // 消息解密密钥,用于解密RMQ消息的Sign
|
||||
NetID *big.Int
|
||||
Config message.ETHConfig
|
||||
ConfirmHeight uint64
|
||||
WsClient *ethclient.Client
|
||||
RpcClient *ethclient.Client
|
||||
MysqlDB *db.MySQLPool
|
||||
SqliteDB *db.SQLite
|
||||
USDT *USDT
|
||||
NetInfo *NetInfo // 网络通用状态(当前高度、gas费用等)
|
||||
// Messages map[string]any // {"queue_id": message.Topup{}, "queue_id": message.Withdraw{}, ...}
|
||||
MessageServer *listen.ListenServer
|
||||
UnConfirmedTxs *UnConfirmedTxs
|
||||
Wallets map[string]*Wallets // {"address" : &Wallets{}, ...}
|
||||
LogsChan chan *types.Header
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type USDT struct {
|
||||
Address common.Address // USDT合约地址
|
||||
ABI abi.ABI // USDT ABI
|
||||
TransferSig common.Hash // USDT函数签名
|
||||
LogsChan chan types.Log
|
||||
Address common.Address // USDT合约地址
|
||||
ListeningAddresses map[string]any // 监听的USDT转账消息
|
||||
ABI abi.ABI // USDT ABI
|
||||
TransferSig common.Hash // USDT函数签名
|
||||
LogsChan chan types.Log
|
||||
}
|
||||
|
||||
func NewETHNode(cfg message.ETHConfig, decodeKey string) (*ETHNode, error) {
|
||||
type UnConfirmedTxs struct {
|
||||
mu sync.Mutex
|
||||
Transactions map[string]message.Transaction // {"queue_id": message.Transactions{}, ...}
|
||||
}
|
||||
|
||||
type NetInfo struct {
|
||||
mu sync.Mutex
|
||||
Height uint64
|
||||
GasLimit uint64
|
||||
GasTipCap *big.Int
|
||||
GasFeeCap *big.Int
|
||||
GasPrice *big.Int // 老版本转账使用的gas
|
||||
}
|
||||
|
||||
type Wallets struct {
|
||||
address string
|
||||
queueId string
|
||||
pk string
|
||||
eth_balance *eth_balance
|
||||
usdt_balance *usdt_balance
|
||||
timestamp uint64
|
||||
sign string
|
||||
status int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type eth_balance struct {
|
||||
symbol string // 默认ETH
|
||||
used_gas *big.Int // 1 ETH = 1e18 WEI
|
||||
balance *big.Int // 实际拥有的ETH余额
|
||||
successed_tx_hash []string
|
||||
failed_tx_hash []string
|
||||
}
|
||||
|
||||
type usdt_balance struct {
|
||||
symbol string
|
||||
freeze_num *big.Int // 1 USDT = 1e6 WEI
|
||||
balance *big.Int // 实际拥有的USDT余额
|
||||
successed_tx_hash []string
|
||||
failed_tx_hash []string
|
||||
}
|
||||
|
||||
func NewETHNode(cfg message.Config, decodeKey string, l *listen.ListenServer) (*ETHNode, error) {
|
||||
// 连入ETH节点的ws
|
||||
ws_client, err := ethclient.Dial(cfg.WsURL)
|
||||
ws_client, err := ethclient.Dial(cfg.ETHConfig.WsUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Ethereum node: %w", err)
|
||||
}
|
||||
// 连入ETH节点的rpc
|
||||
rpc_client, err := ethclient.Dial(cfg.RpcURL)
|
||||
rpc_client, err := ethclient.Dial(cfg.ETHConfig.RpcUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Ethereum node rpc: %w", err)
|
||||
}
|
||||
// 创建可取消的 context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// 获得net_id
|
||||
netId, err := rpc_client.NetworkID(ctx)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to connect to get node net_id: %w", err)
|
||||
}
|
||||
// 构造USDT合约相关
|
||||
usdt := &USDT{}
|
||||
usdt.Address = common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7") // 解析合约地址
|
||||
usdt.ABI = func() abi.ABI { a, _ := abi.JSON(strings.NewReader(erc20ABI)); return a }() // 解析合约ABI
|
||||
usdt.TransferSig = crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")) // 解析合约transfer函数签名
|
||||
usdt.LogsChan = make(chan types.Log, 1000) // 初始化合约日志通道
|
||||
|
||||
// 初始化数据库
|
||||
dbConn, err := db.NewMySQLPool(cfg.DbConfig)
|
||||
// 初始化MySQL数据库
|
||||
dbConn, err := db.NewMySQLPool(cfg.MysqlConfig["wallet"])
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("mysql connect error: %w", err)
|
||||
}
|
||||
|
||||
return ÐNode{
|
||||
decodeKey: decodeKey,
|
||||
NetId: netId,
|
||||
Config: cfg,
|
||||
WsClient: ws_client,
|
||||
RpcClient: rpc_client,
|
||||
Db: *dbConn,
|
||||
ListenAddresses: sync.Map{},
|
||||
UnConfirmTxs: make(map[string]message.Tx_msg),
|
||||
USDT: usdt,
|
||||
RmqMsgs: make(map[string][]any),
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ============================ 抽象接口 ============================
|
||||
func (e *ETHNode) AddAddress(address string, rmq_msg any) {
|
||||
// 统一转换为小写
|
||||
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)
|
||||
// 初始化SQLite3
|
||||
sqlite, err := db.NewSQLite(cfg.ETHConfig.SqlitePath)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("sqlite3 connect error: %w", err)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
func (e *ETHNode) RemoveAddress(address string) {
|
||||
// 统一转换为小写
|
||||
address = strings.ToLower(address)
|
||||
e.ListenAddresses.Delete(address)
|
||||
e.mu.Lock()
|
||||
delete(e.RmqMsgs, address)
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
func (e *ETHNode) Listen(symbol string, ch chan any) {
|
||||
// 启动新区块监听(用于触发交易确认检查)
|
||||
go e.listenNewBlocks("USDT", ch)
|
||||
switch symbol {
|
||||
case "USDT":
|
||||
// 启动 USDT Transfer 事件监听
|
||||
err := e.listen_usdt(ch)
|
||||
if err != nil {
|
||||
log.Fatal("Listen USDT Transactions Error:", err)
|
||||
}
|
||||
logchan := make(chan *types.Header)
|
||||
// 初始化USDT
|
||||
usdt := init_USDT()
|
||||
ethnode := ÐNode{
|
||||
decodeKey: decodeKey,
|
||||
NetID: netId,
|
||||
Config: cfg.ETHConfig,
|
||||
ConfirmHeight: cfg.ETHConfig.ConfirmHeight,
|
||||
WsClient: ws_client,
|
||||
RpcClient: rpc_client,
|
||||
USDT: usdt,
|
||||
MysqlDB: dbConn,
|
||||
SqliteDB: sqlite,
|
||||
NetInfo: &NetInfo{},
|
||||
UnConfirmedTxs: &UnConfirmedTxs{
|
||||
Transactions: make(map[string]message.Transaction),
|
||||
},
|
||||
Wallets: make(map[string]*Wallets),
|
||||
MessageServer: l,
|
||||
LogsChan: logchan,
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
}
|
||||
// 初始化表
|
||||
// err = ethnode.MysqlDB.ExecuteSQLFile("../public/eth_mysql.sql")
|
||||
// if err != nil {
|
||||
// log.Fatalf("ETH-Chain初始化数据库表失败:%v", err)
|
||||
// }
|
||||
// 更新网络公共数据和加载钱包
|
||||
height, err := ethnode.getHeight()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get blockHeight: %v", err)
|
||||
}
|
||||
// 更新网络公共数据
|
||||
ethnode.updateNetInfo(height)
|
||||
// 加载钱包
|
||||
if err := ethnode.loadWallets(); err != nil {
|
||||
return nil, fmt.Errorf("[inital eth wallets]: %v", err)
|
||||
}
|
||||
// 加载未确认交易
|
||||
if err := ethnode.loadUnConfirmedTxs(); err != nil {
|
||||
return nil, fmt.Errorf("load unconfirmtxs error: %v", err)
|
||||
}
|
||||
log.Println("✅ ETH节点已启动")
|
||||
// ethnode.handleHistoryETHEvent(23795552)
|
||||
return ethnode, nil
|
||||
}
|
||||
|
||||
func (e *ETHNode) Transfer(symbol string, msg any) error {
|
||||
switch symbol {
|
||||
case "USDT":
|
||||
err := e.usdt_transfer(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s transfer ERROR: %w", symbol, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported symbol: %s", symbol)
|
||||
// 转账
|
||||
func (e *ETHNode) Transfer(from, to, symbol string, amount, fee float64) error {
|
||||
// 执行转账
|
||||
err := e.handleTx(symbol, from, to, amount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s-tranfer error: %v", symbol, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================ rpc节点方法 ============================
|
||||
|
||||
func (e *ETHNode) getETHBlance(address string) (*big.Int, error) {
|
||||
account := common.HexToAddress(address)
|
||||
ctx := context.Background()
|
||||
balance, err := e.RpcClient.BalanceAt(ctx, account, nil) // nil表示最新高度
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get eth balance:%w", err)
|
||||
}
|
||||
// fBalance := new(big.Float).SetInt(balance)
|
||||
// ethValue := new(big.Float).Quo(fBalance, big.NewFloat(1e18)) // 转 ETH
|
||||
|
||||
// value, _ := ethValue.Float64() // 转 float64
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
func (e *ETHNode) getUSDTBalance(address string) (float64, error) {
|
||||
// 统一转换为小写(common.HexToAddress会自动处理,但为了一致性显式转换)
|
||||
address = strings.ToLower(address)
|
||||
contractAddress := e.USDT.Address
|
||||
accountAddress := common.HexToAddress(address)
|
||||
data, err := e.USDT.ABI.Pack("balanceOf", accountAddress)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to pack balanceOf data: %w", err)
|
||||
}
|
||||
msg := ethereum.CallMsg{
|
||||
To: &contractAddress,
|
||||
Data: data,
|
||||
}
|
||||
// 使用 CallContract 方法查询合约余额
|
||||
res, err := e.RpcClient.CallContract(e.Ctx, msg, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get contract balance: %w", err)
|
||||
}
|
||||
// 解析返回的字节为 *big.Int
|
||||
outputs, err := e.USDT.ABI.Unpack("balanceOf", res)
|
||||
if err != nil || len(outputs) == 0 {
|
||||
return 0, fmt.Errorf("failed to unpack balanceOf result: %w", err)
|
||||
}
|
||||
balance, ok := outputs[0].(*big.Int)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected type for balanceOf result")
|
||||
}
|
||||
bal := utils.BigIntUSDTToFloat64(balance)
|
||||
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) getSuggestGasPrice() (*big.Int, error) {
|
||||
ctx := context.Background()
|
||||
gasPrice, err := e.RpcClient.SuggestGasPrice(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get suggest-gasprice error:%v", err)
|
||||
}
|
||||
return gasPrice, nil
|
||||
}
|
||||
|
||||
// ============================ 业务方法 ============================
|
||||
func (e *ETHNode) listen_usdt(ch chan any) error {
|
||||
fmt.Println("🔍 ETH 开始监听 USDT Transfer 事件...")
|
||||
// 过滤掉非USDT数据
|
||||
query := ethereum.FilterQuery{
|
||||
Addresses: []common.Address{e.USDT.Address},
|
||||
}
|
||||
// 负责重连
|
||||
for {
|
||||
// 订阅日志
|
||||
sub, err := e.WsClient.SubscribeFilterLogs(e.Ctx, query, e.USDT.LogsChan)
|
||||
if err != nil {
|
||||
fmt.Println("❌ 订阅失败, 5秒后重试:", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
fmt.Println("✅ 订阅成功")
|
||||
// 处理事件
|
||||
for {
|
||||
select {
|
||||
case err := <-sub.Err():
|
||||
fmt.Println("⚠️ 订阅异常,准备重连:", err)
|
||||
sub.Unsubscribe() // 清理旧订阅
|
||||
time.Sleep(3 * time.Second)
|
||||
goto reconnect // 跳出内层循环,回到外层重新订阅
|
||||
|
||||
case vLog := <-e.USDT.LogsChan:
|
||||
e.handleUSDTEvent(vLog, ch) // 事件解析 + 分类,传递链消息的通道是vLog而非ch,且一次只传递一笔交易
|
||||
case <-e.Ctx.Done():
|
||||
fmt.Println("🛑 收到停止信号,退出监听")
|
||||
sub.Unsubscribe()
|
||||
return e.Ctx.Err()
|
||||
}
|
||||
}
|
||||
reconnect:
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ETHNode) handleUSDTEvent(vLog types.Log, ch chan any) {
|
||||
from := common.HexToAddress(vLog.Topics[1].Hex())
|
||||
to := common.HexToAddress(vLog.Topics[2].Hex())
|
||||
height := vLog.BlockNumber
|
||||
fromAddr := strings.ToLower(from.Hex())
|
||||
toAddr := strings.ToLower(to.Hex())
|
||||
var transferEvent struct{ Value *big.Int }
|
||||
if err := e.USDT.ABI.UnpackIntoInterface(&transferEvent, "Transfer", vLog.Data); err != nil {
|
||||
fmt.Println("ABI 解析错误:", err)
|
||||
return
|
||||
}
|
||||
// 先验证toAddr是否在监听列表里面
|
||||
_, ok := e.ListenAddresses.Load(toAddr)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tx_hash := vLog.TxHash.Hex()
|
||||
// tx, tx_ok := e.UnConfirmTxs[tx_hash]
|
||||
// if tx_ok {
|
||||
// // 【支付/提现】待确认交易中存在该交易Hash(说明是我们主动发起的交易)
|
||||
// // 直接走确认流程,不发送待确认消息
|
||||
// // log.Printf("🔍 检测到已发起的交易: TxHash=%s, Type=%d", tx_hash, tx.TxType)
|
||||
// e.confirm("USDT", height, tx, ch)
|
||||
// } else {
|
||||
// 【充值】待确认交易中不存在该交易hash(说明是外部转账)
|
||||
// 添加至待确认交易中,并立即发送待确认消息
|
||||
// 1,先根据to查询RmqMsgs,再根据存在的rmq_msg中的相关数据,存入待确认交易
|
||||
value, rmq_msg_ok := e.RmqMsgs[toAddr]
|
||||
var tx_type int
|
||||
if rmq_msg_ok {
|
||||
for _, v := range value {
|
||||
_, ok := v.(message.TopupMsg_req)
|
||||
if ok {
|
||||
tx_type = 0
|
||||
}
|
||||
_, ok1 := v.(message.WithdrawMsg_req)
|
||||
if ok1 {
|
||||
tx_type = 1
|
||||
}
|
||||
_, ok2 := v.(message.PayMsg_req)
|
||||
if ok2 {
|
||||
tx_type = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
e.UnConfirmTxs[tx_hash] = message.Tx_msg{
|
||||
TxType: tx_type,
|
||||
Tx: message.Tx{
|
||||
From: fromAddr,
|
||||
To: toAddr,
|
||||
Height: height,
|
||||
TxHash: tx_hash,
|
||||
Symbol: "USDT",
|
||||
Value: utils.BigIntUSDTToFloat64(transferEvent.Value),
|
||||
Status: 2, // 待确认状态
|
||||
},
|
||||
}
|
||||
// log.Printf("📝 待确认交易新增: TxHash=%s, Height=%d, To=%s, Type=%d", tx_hash, height, toAddr, tx_type)
|
||||
|
||||
// 🔔 【仅充值】立即发送待确认状态的消息(支付/提现不发送待确认消息)
|
||||
if tx_type == 0 && rmq_msg_ok {
|
||||
for _, v := range value {
|
||||
d1, ok := v.(message.TopupMsg_req)
|
||||
if ok && strings.ToLower(d1.Address) == toAddr {
|
||||
pendingMsg := message.TopupMsg_resp{
|
||||
Address: toAddr,
|
||||
Status: 2, // 待确认状态
|
||||
Chain: d1.Chain,
|
||||
Symbol: d1.Symbol,
|
||||
Amount: utils.BigIntUSDTToFloat64(transferEvent.Value),
|
||||
TxHash: tx_hash,
|
||||
}
|
||||
// log.Printf("📤 发送待确认充值消息: TxHash=%s, Address=%s, Amount=%.2f",
|
||||
// tx_hash, toAddr, pendingMsg.Amount)
|
||||
|
||||
// 异步发送,避免阻塞事件处理
|
||||
go func(msg message.TopupMsg_resp) {
|
||||
select {
|
||||
case ch <- msg:
|
||||
log.Printf("✅ 待确认充值消息已发送")
|
||||
default:
|
||||
log.Printf("⚠️ 通道阻塞,待确认消息发送失败")
|
||||
}
|
||||
}(pendingMsg)
|
||||
break
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// listenNewBlocks 监听新区块产生,触发交易确认检查
|
||||
func (e *ETHNode) listenNewBlocks(symbol string, ch chan any) {
|
||||
fmt.Println("🔍 开始监听新区块...")
|
||||
|
||||
headers := make(chan *types.Header, 10)
|
||||
|
||||
// 负责重连
|
||||
for {
|
||||
// 订阅新区块头
|
||||
sub, err := e.WsClient.SubscribeNewHead(e.Ctx, headers)
|
||||
if err != nil {
|
||||
fmt.Println("❌ 订阅新区块失败, 5秒后重试:", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
fmt.Println("✅ 新区块订阅成功")
|
||||
|
||||
// 处理新区块
|
||||
for {
|
||||
select {
|
||||
case err := <-sub.Err():
|
||||
fmt.Println("⚠️ 新区块订阅异常,准备重连:", err)
|
||||
sub.Unsubscribe()
|
||||
time.Sleep(3 * time.Second)
|
||||
goto reconnect
|
||||
|
||||
case header := <-headers:
|
||||
// 每当有新区块,检查待确认交易
|
||||
currentHeight := header.Number.Uint64()
|
||||
// log.Printf("🆕 新区块: %d", currentHeight)
|
||||
|
||||
// 检查是否有待确认交易
|
||||
e.mu.Lock()
|
||||
hasPendingTx := len(e.UnConfirmTxs) > 0
|
||||
e.mu.Unlock()
|
||||
|
||||
if hasPendingTx {
|
||||
e.checkAndConfirmTransactions(symbol, currentHeight, ch)
|
||||
}
|
||||
|
||||
case <-e.Ctx.Done():
|
||||
fmt.Println("🛑 停止新区块监听")
|
||||
sub.Unsubscribe()
|
||||
return
|
||||
}
|
||||
}
|
||||
reconnect:
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndConfirmTransactions 检查并确认达到确认高度的交易
|
||||
func (e *ETHNode) checkAndConfirmTransactions(symbol string, currentHeight uint64, ch chan any) {
|
||||
e.mu.Lock()
|
||||
needConfirmList := []message.Tx_msg{}
|
||||
for _, tx := range e.UnConfirmTxs {
|
||||
// 检查是否达到确认高度
|
||||
if currentHeight >= tx.Tx.Height+e.Config.ConfirmHeight {
|
||||
log.Printf("当前高度=%d, 交易高度=%d", currentHeight, tx.Tx.Height)
|
||||
// log.Printf("✅ 交易达到确认高度: TxHash=%s, 当前高度=%d, 交易高度=%d, 需确认=%d块",
|
||||
// txHash, currentHeight, tx.Tx.Height, e.Config.ConfirmHeight)
|
||||
needConfirmList = append(needConfirmList, tx)
|
||||
}
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
// 批量触发确认(在锁外执行,避免长时间持锁)
|
||||
for _, tx := range needConfirmList {
|
||||
e.confirm(symbol, currentHeight, tx, ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ETHNode) confirm(symbol string, height uint64, tx message.Tx_msg, ch chan any) {
|
||||
switch symbol {
|
||||
case "USDT":
|
||||
e.confirm_usdt(tx, height, ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ETHNode) confirm_usdt(tx message.Tx_msg, height uint64, ch chan any) {
|
||||
// 先从 UnConfirmTxs 中读取
|
||||
e.mu.Lock()
|
||||
unConfirmTx, ok := e.UnConfirmTxs[tx.Tx.TxHash]
|
||||
e.mu.Unlock()
|
||||
|
||||
if !ok {
|
||||
// 监听消息
|
||||
func (e *ETHNode) ListenMsg() {
|
||||
log.Printf("✅ 开始监听msg...")
|
||||
if e.MessageServer.ChToChainServer["ETH"] == nil {
|
||||
log.Printf("ETH消息通道还未建立")
|
||||
return
|
||||
}
|
||||
|
||||
if height < unConfirmTx.Tx.Height {
|
||||
return
|
||||
}
|
||||
|
||||
// 超过确认高度,查询交易数据
|
||||
txHash := common.HexToHash(tx.Tx.TxHash)
|
||||
receipt, err := e.RpcClient.TransactionReceipt(e.Ctx, txHash)
|
||||
var status int
|
||||
if err != nil {
|
||||
log.Println("⚠️ 查询交易收据失败 TxHash=", txHash, err)
|
||||
status = 0
|
||||
} else if receipt.Status == types.ReceiptStatusSuccessful {
|
||||
status = 1
|
||||
} else {
|
||||
status = 0
|
||||
}
|
||||
|
||||
tx.Tx.Status = status
|
||||
|
||||
// 通过通道发送,异步写避免阻塞
|
||||
go func(tx message.Tx_msg) {
|
||||
e.mu.Lock()
|
||||
var result_msg any
|
||||
var matchIndex = -1 // 记录匹配的索引
|
||||
rmq_msg := e.RmqMsgs[tx.Tx.To]
|
||||
for i, v := range rmq_msg {
|
||||
// 处理充值
|
||||
d1, ok := v.(message.TopupMsg_req)
|
||||
if ok {
|
||||
// 统一转小写比较
|
||||
if strings.ToLower(d1.Address) == tx.Tx.To {
|
||||
result_msg = message.TopupMsg_resp{
|
||||
Address: tx.Tx.To,
|
||||
Status: tx.Tx.Status,
|
||||
Chain: d1.Chain,
|
||||
Symbol: d1.Symbol,
|
||||
Amount: tx.Tx.Value,
|
||||
TxHash: tx.Tx.TxHash,
|
||||
}
|
||||
// 充值消息不删除,可能会有多笔充值到同一地址
|
||||
break
|
||||
}
|
||||
}
|
||||
// 处理提现
|
||||
d2, ok1 := v.(message.WithdrawMsg_req)
|
||||
if ok1 {
|
||||
// 统一转小写比较
|
||||
if strings.ToLower(d2.FromAddress) == tx.Tx.From &&
|
||||
strings.ToLower(d2.ToAddress) == tx.Tx.To &&
|
||||
d2.Amount == tx.Tx.Value {
|
||||
result_msg = message.WithdrawMsg_resp{
|
||||
QueueId: d2.QueueId,
|
||||
Status: tx.Tx.Status,
|
||||
Amount: tx.Tx.Value,
|
||||
Chain: d2.Chain,
|
||||
Symbol: d2.Symbol,
|
||||
TxHash: tx.Tx.TxHash,
|
||||
}
|
||||
matchIndex = i // 记录索引,稍后删除
|
||||
break
|
||||
}
|
||||
}
|
||||
// 处理支付
|
||||
d3, ok2 := v.(message.PayMsg_req)
|
||||
if ok2 {
|
||||
// 统一转小写比较
|
||||
if strings.ToLower(d3.FromAddress) == tx.Tx.From &&
|
||||
strings.ToLower(d3.ToAddress) == tx.Tx.To &&
|
||||
d3.Amount == tx.Tx.Value {
|
||||
result_msg = message.PayMsg_resp{
|
||||
QueueId: d3.QueueId,
|
||||
Status: tx.Tx.Status,
|
||||
Amount: tx.Tx.Value,
|
||||
Chain: d3.Chain,
|
||||
Symbol: d3.Symbol,
|
||||
OrderId: d3.OrderId,
|
||||
TxHash: tx.Tx.TxHash,
|
||||
}
|
||||
matchIndex = i // 记录索引,稍后删除
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 循环结束后,统一删除匹配的消息(提现和支付需要删除)
|
||||
if matchIndex >= 0 {
|
||||
e.RmqMsgs[tx.Tx.To] = utils.Slice_delete(e.RmqMsgs[tx.Tx.To], matchIndex)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
select {
|
||||
case ch <- result_msg:
|
||||
for msg := range e.MessageServer.ChToChainServer["ETH"] {
|
||||
switch v := msg.(type) {
|
||||
case message.TopupMsg_req:
|
||||
go e.handleListen_Topup_req(v)
|
||||
case message.WithdrawMsg_req:
|
||||
go e.handleListen_Withdraw_req(v)
|
||||
case message.PayMsg_req:
|
||||
go e.handleListen_Pay_req(v)
|
||||
case message.RemoveListenMsg_req:
|
||||
go e.handleListen_Remove_req(v)
|
||||
default:
|
||||
fmt.Println("⚠️ confirm通道阻塞,消息丢失:", txHash)
|
||||
log.Printf("ListenMsg error: %v", msg)
|
||||
}
|
||||
}(tx)
|
||||
|
||||
// 删除已确认交易
|
||||
e.mu.Lock()
|
||||
delete(e.UnConfirmTxs, tx.Tx.TxHash)
|
||||
e.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ETHNode) decodePrivatekey(address string) string {
|
||||
// 统一转换为小写
|
||||
address = strings.ToLower(address)
|
||||
// 查询加密后的私钥
|
||||
querySql := "SELECT `private_key` FROM eth_balance WHERE address = ? LIMIT 1;"
|
||||
log.Println("查询私钥的钱包地址:", address)
|
||||
var encryptedKey string
|
||||
err := e.Db.QueryRow(querySql, address).Scan(&encryptedKey)
|
||||
if err != nil {
|
||||
log.Println("❌ 查询私钥失败:", err)
|
||||
return ""
|
||||
}
|
||||
// 使用key解密
|
||||
privateKey := encryptedKey // 实际使用时替换成具体的解密代码
|
||||
// fmt.Println(privateKey)
|
||||
return privateKey
|
||||
}
|
||||
// 监听区块链数据
|
||||
func (e *ETHNode) Listen() error {
|
||||
log.Println("✅ 开始监听 ETH 和 USDT 转账事件...")
|
||||
go func() {
|
||||
err := e.listenETHTransactions()
|
||||
if err != nil {
|
||||
log.Printf("Listen ETH Transactions Error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := e.listenUSDTTransactions()
|
||||
if err != nil {
|
||||
log.Printf("Listen USDT Transactions Error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
func (e *ETHNode) usdt_transfer(msg any) error {
|
||||
var user_from, final_from, to string
|
||||
var amount float64
|
||||
// var tx_type int
|
||||
// now_height, err := e.getBlockHeight()
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("get lastest height error: %v", err)
|
||||
// }
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// 断言,确定本次转账是哪个类型
|
||||
// 支付操作
|
||||
v, ok := msg.(message.PayMsg_req)
|
||||
if ok {
|
||||
e.AddAddress(v.ToAddress, v) // 存入该笔msg(AddAddress内部会转小写)
|
||||
// 统一转换为小写
|
||||
user_from, final_from, to, amount = strings.ToLower(v.FromAddress), strings.ToLower(v.FromAddress), strings.ToLower(v.ToAddress), v.Amount
|
||||
// tx_type = 2
|
||||
}
|
||||
// 提现操作
|
||||
k, ok1 := msg.(message.WithdrawMsg_req)
|
||||
if ok1 {
|
||||
e.AddAddress(k.ToAddress, k) // 存入该笔msg(AddAddress内部会转小写)
|
||||
// 统一转换为小写
|
||||
user_from, final_from, to, amount = strings.ToLower(k.FromAddress), strings.ToLower(k.FromAddress), strings.ToLower(k.ToAddress), k.Amount
|
||||
// tx_type = 1
|
||||
}
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// 1,校验钱包余额
|
||||
balance, err := e.getUSDTBalance(user_from)
|
||||
log.Printf("检测Transfer钱包=%s,余额=%f", user_from, balance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get balance: %w", err)
|
||||
}
|
||||
// 2,钱包余额不足,调用归集钱包转账
|
||||
if balance < amount {
|
||||
final_from = "归集钱包"
|
||||
}
|
||||
// 3,通过from地址前往数据库查找出对应加密后的私钥,并解密真实的私钥
|
||||
originalKey := e.decodePrivatekey(final_from)
|
||||
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)
|
||||
}
|
||||
// 4, 获得nonce
|
||||
nonce, err := e.RpcClient.PendingNonceAt(e.Ctx, common.HexToAddress(final_from))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nonce: %w", err)
|
||||
}
|
||||
// 5, 构造交易(ERC20 transfer 调用)
|
||||
amountBigInt := utils.Float64ToBigIntUSDT(amount)
|
||||
data, err := e.USDT.ABI.Pack("transfer", common.HexToAddress(to), amountBigInt) // 打包 transfer(address,uint256) 方法调用
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pack transfer data: %w", err)
|
||||
}
|
||||
gasPrice, err := e.getSuggestGasPrice() // 获得当前建议gasPrice
|
||||
if err != nil {
|
||||
return fmt.Errorf("get suggest-gasprice error:%v", err)
|
||||
}
|
||||
eth_balance, err := e.getETHBlance(final_from) // 获得钱包eth余额
|
||||
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)
|
||||
// 判断钱包eth是否支持本次交易gas费用
|
||||
if eth_balance.Cmp(gas) == -1 {
|
||||
return fmt.Errorf("address=%s balance less than gas=%v(wei)", final_from, eth_balance)
|
||||
}
|
||||
// 构造发送到 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
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign transaction: %w", err)
|
||||
}
|
||||
// 7, 发送交易
|
||||
err = e.RpcClient.SendTransaction(e.Ctx, signedTx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send transaction: %w", err)
|
||||
}
|
||||
// // 8, 构造交易消息
|
||||
// tx_msg := message.Tx_msg{
|
||||
// TxType: tx_type,
|
||||
// Tx: message.Tx{
|
||||
// From: final_from,
|
||||
// To: to,
|
||||
// Height: now_height,
|
||||
// TxHash: txHash,
|
||||
// Symbol: "USDT",
|
||||
// Value: amount,
|
||||
// Status: 2,
|
||||
// },
|
||||
// }
|
||||
// // 9, 将构造的交易消息存入待确认交易中
|
||||
// e.UnConfirmTxs[txHash] = tx_msg
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *ETHNode) Stop() {
|
||||
e.Cancel()
|
||||
if e.Cancel != nil {
|
||||
e.Cancel()
|
||||
}
|
||||
log.Println("🛑 停止监听...")
|
||||
}
|
||||
|
||||
1611
internal/blockchain/eth/eth_prv.go
Normal file
1611
internal/blockchain/eth/eth_prv.go
Normal file
File diff suppressed because it is too large
Load Diff
13
internal/blockchain/tron/tron.go
Normal file
13
internal/blockchain/tron/tron.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package tron
|
||||
|
||||
// 区块
|
||||
// {
|
||||
// "blockID":,
|
||||
// "block_header": map,
|
||||
// "transactions": map,
|
||||
//}
|
||||
|
||||
type TRONNode struct {
|
||||
decodeKey string
|
||||
ConfirmHeight uint64
|
||||
}
|
||||
1
internal/blockchain/tron/tron_prv.go
Normal file
1
internal/blockchain/tron/tron_prv.go
Normal file
@@ -0,0 +1 @@
|
||||
package tron
|
||||
44
internal/constant/constant.go
Normal file
44
internal/constant/constant.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
STATUS_FAILED = 0 // 失败
|
||||
STATUS_SUCCESS = 1 // 成功
|
||||
STATUS_PENDING = 2 // 待确认
|
||||
STATUS_VERIFY_FAILED = 3 // 验证失败
|
||||
STATUS_BALANCE_NOT_ENOUGH = 4 // 钱包余额不足
|
||||
STATUS_UNTRANSFER = 5 // 未支付
|
||||
STATUS_END = 6 // 完成
|
||||
STATUS_ERROR = 7
|
||||
)
|
||||
const ETH_ERC20_USDT_CONTRACT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
|
||||
const ETH_ERC20_ABI = `
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{"name": "_owner", "type": "address"}],
|
||||
"name": "balanceOf",
|
||||
"outputs": [{"name": "balance", "type": "uint256"}],
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{"name": "_to", "type": "address"},
|
||||
{"name": "_value", "type": "uint256"}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [{"name": "", "type": "bool"}],
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{"indexed": true, "name": "from", "type": "address"},
|
||||
{"indexed": true, "name": "to", "type": "address"},
|
||||
{"indexed": false,"name": "value","type": "uint256"}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
}
|
||||
]
|
||||
`
|
||||
@@ -1,70 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
message "m2pool-payment/internal/msg"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type MySQLPool struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewMySQLPool 初始化连接池
|
||||
func NewMySQLPool(cfg message.DbConfig) (*MySQLPool, error) {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
db.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
db.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
db.SetConnMaxLifetime(cfg.ConnMaxLife)
|
||||
|
||||
// 测试连接
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MySQLPool{db: db}, nil
|
||||
}
|
||||
|
||||
// Exec 执行 INSERT/UPDATE/DELETE
|
||||
func (p *MySQLPool) Exec(query string, args ...any) (sql.Result, error) {
|
||||
return p.db.Exec(query, args...)
|
||||
}
|
||||
|
||||
// Query 查询多行
|
||||
func (p *MySQLPool) Query(query string, args ...any) (*sql.Rows, error) {
|
||||
return p.db.Query(query, args...)
|
||||
}
|
||||
|
||||
// QueryRow 查询单行
|
||||
func (p *MySQLPool) QueryRow(query string, args ...any) *sql.Row {
|
||||
return p.db.QueryRow(query, args...)
|
||||
}
|
||||
|
||||
// Transaction 执行事务
|
||||
func (p *MySQLPool) Transaction(fn func(tx *sql.Tx) error) error {
|
||||
tx, err := p.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fn(tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Close 关闭连接池
|
||||
func (p *MySQLPool) Close() error {
|
||||
return p.db.Close()
|
||||
}
|
||||
218
internal/db/mysql.go
Normal file
218
internal/db/mysql.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
message "m2pool-payment/internal/msg"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type MySQLPool struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewMySQLPool 初始化连接池
|
||||
func NewMySQLPool(cfg message.MysqlConfig) (*MySQLPool, error) {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
db.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
db.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
db.SetConnMaxLifetime(cfg.ConnMaxLife)
|
||||
|
||||
// 测试连接
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MySQLPool{db: db}, nil
|
||||
}
|
||||
|
||||
// splitSQLStatements 将 SQL 内容按分号分割
|
||||
func splitSQLStatements(sqlContent string) []string {
|
||||
// 使用 bufio 扫描文件内容
|
||||
var statements []string
|
||||
scanner := bufio.NewScanner(strings.NewReader(sqlContent))
|
||||
var queryBuilder strings.Builder
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// 处理每一行的 SQL 语句
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果行中包含分号,说明是完整的 SQL 语句
|
||||
queryBuilder.WriteString(line)
|
||||
if strings.HasSuffix(line, ";") {
|
||||
statements = append(statements, queryBuilder.String())
|
||||
queryBuilder.Reset() // 清空构建器以便准备下一条 SQL 语句
|
||||
} else {
|
||||
queryBuilder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 处理可能的扫描错误
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatalf("error reading .sql file: %v\n", err)
|
||||
}
|
||||
|
||||
return statements
|
||||
}
|
||||
|
||||
func (p *MySQLPool) ExecuteSQLFile(filePath string) error {
|
||||
// 读取 SQL 文件内容
|
||||
sqlContent, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read SQL file: %v", err)
|
||||
}
|
||||
// 将文件内容按分号 (;) 分割成多条 SQL 语句
|
||||
queries := splitSQLStatements(string(sqlContent))
|
||||
|
||||
// 执行每一条 SQL 语句
|
||||
for _, query := range queries {
|
||||
// 跳过空行或注释
|
||||
if strings.TrimSpace(query) == "" || strings.HasPrefix(strings.TrimSpace(query), "--") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 执行 SQL 语句
|
||||
_, err := p.db.Exec(query)
|
||||
if err != nil {
|
||||
log.Printf("error executing query: %v\n", err)
|
||||
} else {
|
||||
// fmt.Println("Executed query:", query)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exec 执行 INSERT/UPDATE/DELETE
|
||||
func (p *MySQLPool) Exec(query string, args ...any) (sql.Result, error) {
|
||||
return p.db.Exec(query, args...)
|
||||
}
|
||||
|
||||
// Query 查询多行
|
||||
func (p *MySQLPool) Query(query string, args ...any) (*sql.Rows, error) {
|
||||
return p.db.Query(query, args...)
|
||||
}
|
||||
|
||||
// QueryRow 查询单行
|
||||
func (p *MySQLPool) QueryRow(query string, args ...any) *sql.Row {
|
||||
return p.db.QueryRow(query, args...)
|
||||
}
|
||||
|
||||
// Transaction 执行事务
|
||||
func (p *MySQLPool) Transaction(fn func(tx *sql.Tx) error) error {
|
||||
tx, err := p.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fn(tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Insert 执行通用插入操作
|
||||
func (p *MySQLPool) Insert(query string, values [][]any) (sql.Result, error) {
|
||||
// 预处理查询
|
||||
stmt, err := p.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare statement: %v", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
// 执行批量插入
|
||||
var result sql.Result
|
||||
for _, row := range values {
|
||||
result, err = stmt.Exec(row...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute insert: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Delete 执行通用删除操作
|
||||
|
||||
// Update 执行通用更新操作
|
||||
func (p *MySQLPool) Update(query string, values []any) (sql.Result, error) {
|
||||
// 预处理查询
|
||||
stmt, err := p.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare statement: %v", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
// 执行更新操作
|
||||
result, err := stmt.Exec(values...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute update: %v", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ExecuteTransactions 执行多条增删改操作,确保事务的原子性
|
||||
func (p *MySQLPool) ExecuteTransactions(str_sqls []string, params [][]any) error {
|
||||
// 检查 SQL 和参数的数量是否匹配
|
||||
if len(str_sqls) != len(params) {
|
||||
return fmt.Errorf("sql length != params length")
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
tx, err := p.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %v", err)
|
||||
}
|
||||
|
||||
// 确保在函数结束时提交或回滚事务
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// 发生错误时回滚事务
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
err = fmt.Errorf("failed to rollback transaction: %v", rollbackErr)
|
||||
}
|
||||
} else {
|
||||
// 如果没有错误,提交事务
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
err = fmt.Errorf("failed to commit transaction: %v", commitErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 执行每个 SQL 语句
|
||||
for i, sql_str := range str_sqls {
|
||||
// 使用事务对象 tx 来执行 SQL
|
||||
_, err := tx.Exec(sql_str, params[i]...)
|
||||
if err != nil {
|
||||
// 如果执行失败,立即返回并且触发回滚
|
||||
return fmt.Errorf("failed to execute SQL at index %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有 SQL 执行成功,则返回 nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭连接池
|
||||
func (p *MySQLPool) Close() error {
|
||||
return p.db.Close()
|
||||
}
|
||||
167
internal/db/sqlite.go
Normal file
167
internal/db/sqlite.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "modernc.org/sqlite" // 导入驱动
|
||||
)
|
||||
|
||||
type SQLite struct {
|
||||
DB *sql.DB
|
||||
// Ch chan any
|
||||
}
|
||||
|
||||
// 初始化连接
|
||||
func NewSQLite(path string) (*SQLite, error) {
|
||||
db, err := sql.Open("sqlite", path)
|
||||
// ch := make(chan any, 1000)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open sqlite failed: %v", err)
|
||||
}
|
||||
return &SQLite{
|
||||
DB: db,
|
||||
// Ch: ch,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 关闭数据库
|
||||
func (s *SQLite) Close() {
|
||||
if s.DB != nil {
|
||||
s.DB.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 新建表
|
||||
func (s *SQLite) CreateTable(sql string) error {
|
||||
_, err := s.DB.Exec(sql)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Exec DB error: %w", err)
|
||||
}
|
||||
return 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) 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
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
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
|
||||
}
|
||||
|
||||
// 通用查询方法(返回[]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) ExecuteTransactions(str_sqls []string, params [][]any) error {
|
||||
// 检查 SQL 和参数的数量是否匹配
|
||||
if len(str_sqls) != len(params) {
|
||||
return fmt.Errorf("sql length != params length")
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
tx, err := s.DB.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %v", err)
|
||||
}
|
||||
|
||||
// 确保在函数结束时提交或回滚事务
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// 发生错误时回滚事务
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
err = fmt.Errorf("failed to rollback transaction: %v", rollbackErr)
|
||||
}
|
||||
} else {
|
||||
// 如果没有错误,提交事务
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
err = fmt.Errorf("failed to commit transaction: %v", commitErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 执行每个 SQL 语句
|
||||
for i, sql_str := range str_sqls {
|
||||
// 使用事务对象 tx 来执行 SQL
|
||||
_, err := tx.Exec(sql_str, params[i]...)
|
||||
if err != nil {
|
||||
// 如果执行失败,立即返回并且触发回滚
|
||||
return fmt.Errorf("failed to execute SQL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有 SQL 执行成功,则返回 nil
|
||||
return nil
|
||||
}
|
||||
336
internal/listen/listen.go
Normal file
336
internal/listen/listen.go
Normal file
@@ -0,0 +1,336 @@
|
||||
package listen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"m2pool-payment/internal/db"
|
||||
message "m2pool-payment/internal/msg"
|
||||
"m2pool-payment/internal/utils"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ListenServer struct {
|
||||
Config message.Config
|
||||
MysqlDB *db.MySQLPool
|
||||
SqliteDB *db.SQLite
|
||||
TopupMsgs map[string]*TopupMsgs // {"ETH": TopupMsgs{"queue_id": message.Topupmsg_req{}, ...}}
|
||||
WithdrawMsgs map[string]*WithdrawMsgs
|
||||
PayMsgs map[string]*PayMsgs
|
||||
RemoveMsgs map[string]*RemoveMsgs
|
||||
ChToChainServer map[string]chan any
|
||||
ChFromChainServer map[string]chan any
|
||||
ChFromRmqServer chan any
|
||||
ChToRmqServer chan any
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type TopupMsgs struct {
|
||||
Msgs map[string]message.TopupMsg_req // {"queue_id": message.Topupmsg_req{}, ...}
|
||||
mu sync.RWMutex // 读写锁
|
||||
}
|
||||
|
||||
type WithdrawMsgs struct {
|
||||
Msgs map[string]message.WithdrawMsg_req
|
||||
mu sync.RWMutex // 读写锁
|
||||
}
|
||||
|
||||
type PayMsgs struct {
|
||||
Msgs map[string]message.PayMsg_req
|
||||
mu sync.RWMutex // 读写锁
|
||||
}
|
||||
|
||||
type RemoveMsgs struct {
|
||||
Msgs map[string]message.RemoveListenMsg_req
|
||||
mu sync.RWMutex // 读写锁
|
||||
}
|
||||
|
||||
func NewListenServer(cfg message.Config) *ListenServer {
|
||||
// 初始化MySQL数据库
|
||||
dbConn, err := db.NewMySQLPool(cfg.MysqlConfig["wallet"])
|
||||
if err != nil {
|
||||
log.Printf("mysql connect error: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 初始化SQLite3
|
||||
sqlite, err := db.NewSQLite(cfg.MsgConfig.SqlitePath)
|
||||
if err != nil {
|
||||
log.Printf("sqlite3 connect error: %v", err)
|
||||
return nil
|
||||
}
|
||||
nets := cfg.Net
|
||||
topup_msgs := make(map[string]*TopupMsgs)
|
||||
withdraw_msgs := make(map[string]*WithdrawMsgs)
|
||||
pay_msgs := make(map[string]*PayMsgs)
|
||||
remove_msgs := make(map[string]*RemoveMsgs)
|
||||
for _, net := range nets {
|
||||
topup_msgs[net] = &TopupMsgs{
|
||||
Msgs: make(map[string]message.TopupMsg_req),
|
||||
}
|
||||
withdraw_msgs[net] = &WithdrawMsgs{
|
||||
Msgs: make(map[string]message.WithdrawMsg_req),
|
||||
}
|
||||
pay_msgs[net] = &PayMsgs{
|
||||
Msgs: make(map[string]message.PayMsg_req),
|
||||
}
|
||||
remove_msgs[net] = &RemoveMsgs{
|
||||
Msgs: make(map[string]message.RemoveListenMsg_req),
|
||||
}
|
||||
}
|
||||
ch := make(map[string]chan any)
|
||||
chin := make(map[string]chan any)
|
||||
rmq_ch_in := make(chan any, 1000)
|
||||
rmq_ch_out := make(chan any, 1000)
|
||||
for _, net := range cfg.Net {
|
||||
ch[net] = make(chan any, 1000)
|
||||
chin[net] = make(chan any, 1000)
|
||||
}
|
||||
var l = &ListenServer{
|
||||
Config: cfg,
|
||||
MysqlDB: dbConn,
|
||||
SqliteDB: sqlite,
|
||||
TopupMsgs: topup_msgs,
|
||||
WithdrawMsgs: withdraw_msgs,
|
||||
PayMsgs: pay_msgs,
|
||||
RemoveMsgs: remove_msgs,
|
||||
ChToChainServer: ch,
|
||||
ChFromChainServer: chin,
|
||||
ChFromRmqServer: rmq_ch_in,
|
||||
ChToRmqServer: rmq_ch_out,
|
||||
}
|
||||
// err = l.MysqlDB.ExecuteSQLFile("../public/msg_mysql.sql")
|
||||
// if err != nil {
|
||||
// log.Fatalf("Listen-message初始化数据库表失败:%v", err)
|
||||
// }
|
||||
l.loadMsg()
|
||||
log.Println("✅ 消息监听处理已启动")
|
||||
return l
|
||||
}
|
||||
|
||||
// rmq -> listen -> chain server
|
||||
func (l *ListenServer) RmqMsgIn() {
|
||||
for msg := range l.ChFromRmqServer {
|
||||
switch v := msg.(type) {
|
||||
case message.TopupMsg_req:
|
||||
if !utils.Contains(l.Config.Net, v.Chain) {
|
||||
log.Printf("%s has not support", v.Chain)
|
||||
continue
|
||||
}
|
||||
go l.handleRmqTopup_req(v)
|
||||
case message.WithdrawMsg_req:
|
||||
if !utils.Contains(l.Config.Net, v.Chain) {
|
||||
log.Printf("%s has not support", v.Chain)
|
||||
continue
|
||||
}
|
||||
go l.handleRmqWithdraw_req(v)
|
||||
case message.PayMsg_req:
|
||||
if !utils.Contains(l.Config.Net, v.Chain) {
|
||||
log.Printf("%s has not support", v.Chain)
|
||||
continue
|
||||
}
|
||||
go l.handleRmqPay_req(v)
|
||||
case message.RemoveListenMsg_req:
|
||||
if !utils.Contains(l.Config.Net, v.Chain) {
|
||||
log.Printf("%s has not support", v.Chain)
|
||||
continue
|
||||
}
|
||||
go l.handleRmqRemove_req(v)
|
||||
default:
|
||||
log.Printf("ListenMsg error: %v", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// chain server -> listen -> rmq
|
||||
func (l *ListenServer) NetMsgIn() {
|
||||
for _, ch := range l.ChFromChainServer {
|
||||
for msg := range ch {
|
||||
switch v := msg.(type) {
|
||||
// 接收到区块链节点返回的更新req状态消息
|
||||
case message.UpdateReqState:
|
||||
go l.handleUpdateReqState(v)
|
||||
// 接收到区块链节点服务返回的resp消息,除了修改相关状态和数据库,还需通过rmq_out通道返回出去
|
||||
case message.TopupMsg_resp:
|
||||
go l.handleChainTopup_resp(v)
|
||||
case message.WithdrawMsg_resp:
|
||||
go l.handleChainWithdraw_resp(v)
|
||||
case message.PayMsg_resp:
|
||||
go l.handleChainPay_resp(v)
|
||||
case message.RemoveListenMsg_resp:
|
||||
go l.handleChainRemove_resp(v)
|
||||
default:
|
||||
log.Printf("MsgType not found: %v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据 address 查找对应的消息
|
||||
func (l *ListenServer) FindMsgWithAddress(chain, address string) (any, error) {
|
||||
// 检查链是否支持
|
||||
if !utils.Contains(l.Config.Net, chain) {
|
||||
return nil, fmt.Errorf("%s has not support", chain)
|
||||
}
|
||||
|
||||
// 查找 TopupMsgs
|
||||
l.TopupMsgs[chain].mu.RLock()
|
||||
for _, topup_msg := range l.TopupMsgs[chain].Msgs {
|
||||
if address == topup_msg.Address {
|
||||
l.TopupMsgs[chain].mu.RUnlock()
|
||||
return topup_msg, nil
|
||||
}
|
||||
}
|
||||
l.TopupMsgs[chain].mu.RUnlock()
|
||||
|
||||
// 查找 WithdrawMsgs
|
||||
l.WithdrawMsgs[chain].mu.RLock()
|
||||
for _, withdraw_msg := range l.WithdrawMsgs[chain].Msgs {
|
||||
if address == withdraw_msg.FromAddress {
|
||||
l.WithdrawMsgs[chain].mu.RUnlock()
|
||||
return withdraw_msg, nil
|
||||
}
|
||||
}
|
||||
l.WithdrawMsgs[chain].mu.RUnlock()
|
||||
|
||||
// 查找 PayMsgs
|
||||
l.PayMsgs[chain].mu.RLock()
|
||||
for _, pay_msg := range l.PayMsgs[chain].Msgs {
|
||||
if address == pay_msg.FromAddress {
|
||||
l.PayMsgs[chain].mu.RUnlock()
|
||||
return pay_msg, nil
|
||||
}
|
||||
}
|
||||
l.PayMsgs[chain].mu.RUnlock()
|
||||
|
||||
// 如果没有找到匹配项,返回错误
|
||||
return nil, fmt.Errorf("Address(%s) not found in any message", address)
|
||||
}
|
||||
|
||||
// 根据address查找Topup消息
|
||||
func (l *ListenServer) FindTopupMsgWithToaddress(chain, toAddress string) (message.TopupMsg_req, error) {
|
||||
// 检查链是否支持
|
||||
if !utils.Contains(l.Config.Net, chain) {
|
||||
return message.TopupMsg_req{}, fmt.Errorf("%s has not support", chain)
|
||||
}
|
||||
l.TopupMsgs[chain].mu.RLock()
|
||||
for _, topup_msg := range l.TopupMsgs[chain].Msgs {
|
||||
if toAddress == topup_msg.Address {
|
||||
l.TopupMsgs[chain].mu.RUnlock()
|
||||
return topup_msg, nil
|
||||
}
|
||||
}
|
||||
l.TopupMsgs[chain].mu.RUnlock()
|
||||
return message.TopupMsg_req{}, fmt.Errorf("Address(%s) not found in topup message", toAddress)
|
||||
}
|
||||
|
||||
// 根据address查找Topup消息
|
||||
func (l *ListenServer) FindWithdrawMsgWithToaddress(chain, fromAddress string) (message.WithdrawMsg_req, error) {
|
||||
// 检查链是否支持
|
||||
if !utils.Contains(l.Config.Net, chain) {
|
||||
return message.WithdrawMsg_req{}, fmt.Errorf("%s has not support", chain)
|
||||
}
|
||||
l.WithdrawMsgs[chain].mu.RLock()
|
||||
for _, withdraw_msg := range l.WithdrawMsgs[chain].Msgs {
|
||||
if fromAddress == withdraw_msg.FromAddress {
|
||||
l.WithdrawMsgs[chain].mu.RUnlock()
|
||||
return withdraw_msg, nil
|
||||
}
|
||||
}
|
||||
l.WithdrawMsgs[chain].mu.RUnlock()
|
||||
return message.WithdrawMsg_req{}, fmt.Errorf("Address(%s) not found in withdraw message", fromAddress)
|
||||
}
|
||||
|
||||
// 根据address查找Topup消息
|
||||
func (l *ListenServer) FindPayMsgWithToaddress(chain, fromAddress string) (message.PayMsg_req, error) {
|
||||
// 检查链是否支持
|
||||
if !utils.Contains(l.Config.Net, chain) {
|
||||
return message.PayMsg_req{}, fmt.Errorf("%s has not support", chain)
|
||||
}
|
||||
l.PayMsgs[chain].mu.RLock()
|
||||
for _, pay_msg := range l.PayMsgs[chain].Msgs {
|
||||
if fromAddress == pay_msg.FromAddress {
|
||||
l.PayMsgs[chain].mu.RUnlock()
|
||||
return pay_msg, nil
|
||||
}
|
||||
}
|
||||
l.PayMsgs[chain].mu.RUnlock()
|
||||
return message.PayMsg_req{}, fmt.Errorf("Address(%s) not found in pay message", fromAddress)
|
||||
}
|
||||
|
||||
// 根据queue_id查找到对应消息
|
||||
func (l *ListenServer) FindMsgWithQueueID(chain, queue_id string, txType int) (any, error) {
|
||||
if !utils.Contains(l.Config.Net, chain) {
|
||||
return nil, fmt.Errorf("%s has not support", chain)
|
||||
}
|
||||
|
||||
switch txType {
|
||||
case 0:
|
||||
l.TopupMsgs[chain].mu.RLock()
|
||||
defer l.TopupMsgs[chain].mu.RUnlock()
|
||||
|
||||
if msg, ok := l.TopupMsgs[chain].Msgs[queue_id]; ok {
|
||||
return msg, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("QueueID(%s) not found in txType(%d)", queue_id, txType)
|
||||
}
|
||||
case 1:
|
||||
l.WithdrawMsgs[chain].mu.RLock()
|
||||
defer l.WithdrawMsgs[chain].mu.RUnlock()
|
||||
|
||||
if msg, ok := l.WithdrawMsgs[chain].Msgs[queue_id]; ok {
|
||||
return msg, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("QueueID(%s) not found in txType(%d)", queue_id, txType)
|
||||
}
|
||||
case 2:
|
||||
l.PayMsgs[chain].mu.RLock()
|
||||
defer l.PayMsgs[chain].mu.RUnlock()
|
||||
|
||||
if msg, ok := l.PayMsgs[chain].Msgs[queue_id]; ok {
|
||||
return msg, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("QueueID(%s) not found in txType(%d)", queue_id, txType)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("This txType(%d) don`t support:txType(0,1,2)", txType)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ListenServer) ReadTopupMsgs(chain string) (map[string]message.TopupMsg_req, error) {
|
||||
l.TopupMsgs[chain].mu.RLock()
|
||||
defer l.TopupMsgs[chain].mu.RUnlock()
|
||||
|
||||
var topup_msgs = make(map[string]message.TopupMsg_req)
|
||||
if !utils.Contains(l.Config.Net, chain) {
|
||||
return topup_msgs, fmt.Errorf("%s has not support", chain)
|
||||
}
|
||||
|
||||
topup_msgs = l.TopupMsgs[chain].Msgs
|
||||
return topup_msgs, nil
|
||||
}
|
||||
|
||||
func (l *ListenServer) ReadWithdrawMsgs(chain string) (map[string]message.WithdrawMsg_req, error) {
|
||||
l.WithdrawMsgs[chain].mu.RLock()
|
||||
defer l.WithdrawMsgs[chain].mu.RUnlock()
|
||||
|
||||
var withdraw_msgs = make(map[string]message.WithdrawMsg_req)
|
||||
if !utils.Contains(l.Config.Net, chain) {
|
||||
return withdraw_msgs, fmt.Errorf("%s has not support", chain)
|
||||
}
|
||||
|
||||
withdraw_msgs = l.WithdrawMsgs[chain].Msgs
|
||||
return withdraw_msgs, nil
|
||||
}
|
||||
|
||||
func (l *ListenServer) ReadPayMsgs(chain string) (map[string]message.PayMsg_req, error) {
|
||||
l.PayMsgs[chain].mu.RLock()
|
||||
defer l.PayMsgs[chain].mu.RUnlock()
|
||||
|
||||
var pay_msgs = make(map[string]message.PayMsg_req)
|
||||
if !utils.Contains(l.Config.Net, chain) {
|
||||
return pay_msgs, fmt.Errorf("%s has not support", chain)
|
||||
}
|
||||
|
||||
pay_msgs = l.PayMsgs[chain].Msgs
|
||||
return pay_msgs, nil
|
||||
}
|
||||
547
internal/listen/listen_prv.go
Normal file
547
internal/listen/listen_prv.go
Normal file
@@ -0,0 +1,547 @@
|
||||
package listen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"m2pool-payment/internal/constant"
|
||||
message "m2pool-payment/internal/msg"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 加载所有消息
|
||||
|
||||
func (l *ListenServer) loadMsg() {
|
||||
topup_sql := "SELECT queue_id, chain, symbol, address, timestamp, sign, status FROM topup_req_msg WHERE status = ?" // 1正在监听
|
||||
withdraw_sql := "SELECT queue_id, chain, symbol, from_addr, to_addr, amount, fee, timestamp, sign, status FROM withdraw_req_msg WHERE status = ? OR status = ?" // 2待确认,5待支付
|
||||
pay_sql := "SELECT queue_id, chain, symbol, from_addr, to_addr, amount, fee, timestamp, sign, status FROM withdraw_req_msg WHERE status = ? OR status = ?" // 2待确认,5待支付
|
||||
remove_sql := "SELECT queue_id, msg_type, chain, symbol, address, timestamp, sign, status FROM remove_req_msg WHERE status = ?" // 2待执行
|
||||
topup_params := []any{1}
|
||||
withdraw_params := []any{2, 5}
|
||||
pay_params := []any{2, 5}
|
||||
remove_params := []any{2}
|
||||
|
||||
// 处理充值消息
|
||||
go func() {
|
||||
rows, err := l.MysqlDB.Query(topup_sql, topup_params...)
|
||||
if err != nil {
|
||||
log.Fatalf("load topup-msg error: %v", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close() // 确保在函数结束时关闭 rows
|
||||
|
||||
// 遍历查询结果
|
||||
for rows.Next() {
|
||||
var queueID, chain, symbol, address, sign string
|
||||
var timestamp int64
|
||||
var status int
|
||||
|
||||
// 扫描每行数据到相应的变量
|
||||
err := rows.Scan(&queueID, &chain, &symbol, &address, ×tamp, &sign, &status)
|
||||
if err != nil {
|
||||
log.Printf("failed to scan topup row: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
l.TopupMsgs[chain].mu.RLock()
|
||||
l.TopupMsgs[chain].Msgs[queueID] = message.TopupMsg_req{
|
||||
QueueId: queueID,
|
||||
Chain: chain,
|
||||
Symbol: symbol,
|
||||
Address: address,
|
||||
Timestamp: uint64(timestamp),
|
||||
Sign: sign,
|
||||
Status: status,
|
||||
}
|
||||
l.TopupMsgs[chain].mu.RUnlock()
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Printf("error occurred during rows iteration: %v", err)
|
||||
}
|
||||
log.Printf("充值历史消息load完毕")
|
||||
}()
|
||||
|
||||
// 处理提现消息
|
||||
go func() {
|
||||
rows, err := l.MysqlDB.Query(withdraw_sql, withdraw_params...)
|
||||
if err != nil {
|
||||
log.Fatalf("load withdraw-msg error: %v", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// 遍历查询结果
|
||||
for rows.Next() {
|
||||
var queueID, chain, symbol, fromAddr, toAddr, amountStr, feeStr, sign string
|
||||
var timestamp int64
|
||||
var status int
|
||||
|
||||
// 扫描每行数据
|
||||
err := rows.Scan(&queueID, &chain, &symbol, &fromAddr, &toAddr, &amountStr, &feeStr, ×tamp, &sign, &status)
|
||||
if err != nil {
|
||||
log.Printf("failed to scan withdraw row: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 将 amount 和 fee 转换为浮动数字
|
||||
amount, err := strconv.ParseFloat(amountStr, 64)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse amount: %v", err)
|
||||
continue
|
||||
}
|
||||
fee, err := strconv.ParseFloat(feeStr, 64)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse fee: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
l.WithdrawMsgs[chain].mu.RLock()
|
||||
l.WithdrawMsgs[chain].Msgs[queueID] = message.WithdrawMsg_req{
|
||||
QueueId: queueID,
|
||||
Chain: chain,
|
||||
Symbol: symbol,
|
||||
FromAddress: fromAddr,
|
||||
ToAddress: toAddr,
|
||||
Amount: amount,
|
||||
Fee: fee,
|
||||
Timestamp: uint64(timestamp),
|
||||
Sign: sign,
|
||||
Status: status,
|
||||
}
|
||||
l.WithdrawMsgs[chain].mu.RUnlock()
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Printf("error occurred during rows iteration: %v", err)
|
||||
}
|
||||
log.Printf("提现历史消息load完毕")
|
||||
}()
|
||||
|
||||
// 处理支付消息
|
||||
go func() {
|
||||
rows, err := l.MysqlDB.Query(pay_sql, pay_params...)
|
||||
if err != nil {
|
||||
log.Fatalf("load pay-msg error: %v", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// 遍历查询结果
|
||||
for rows.Next() {
|
||||
var queueID, chain, symbol, fromAddr, toAddr, amountStr, feeStr, sign string
|
||||
var timestamp int64
|
||||
var status int
|
||||
|
||||
// 扫描每行数据
|
||||
err := rows.Scan(&queueID, &chain, &symbol, &fromAddr, &toAddr, &amountStr, &feeStr, ×tamp, &sign, &status)
|
||||
if err != nil {
|
||||
log.Printf("failed to scan pay row: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 将 amount 和 fee 转换为浮动数字
|
||||
amount, err := strconv.ParseFloat(amountStr, 64)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse amount: %v", err)
|
||||
continue
|
||||
}
|
||||
fee, err := strconv.ParseFloat(feeStr, 64)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse fee: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
l.PayMsgs[chain].mu.RLock()
|
||||
l.PayMsgs[chain].Msgs[queueID] = message.PayMsg_req{
|
||||
QueueId: queueID,
|
||||
Chain: chain,
|
||||
Symbol: symbol,
|
||||
FromAddress: fromAddr,
|
||||
ToAddress: toAddr,
|
||||
Amount: amount,
|
||||
Fee: fee,
|
||||
Timestamp: uint64(timestamp),
|
||||
Sign: sign,
|
||||
Status: status,
|
||||
}
|
||||
l.PayMsgs[chain].mu.RUnlock()
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Printf("error occurred during rows iteration: %v", err)
|
||||
}
|
||||
log.Printf("支付历史消息load完毕")
|
||||
}()
|
||||
|
||||
// 处理移除消息
|
||||
go func() {
|
||||
rows, err := l.MysqlDB.Query(remove_sql, remove_params...)
|
||||
if err != nil {
|
||||
log.Fatalf("load remove-msg error: %v", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// 遍历查询结果
|
||||
for rows.Next() {
|
||||
var queueID, chain, symbol, address, sign string
|
||||
var timestamp int64
|
||||
var status, msgType int
|
||||
|
||||
// 扫描每行数据
|
||||
err := rows.Scan(&queueID, &msgType, &chain, &symbol, &address, ×tamp, &sign, &status)
|
||||
if err != nil {
|
||||
log.Printf("failed to scan remove row: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
l.RemoveMsgs[chain].mu.RLock()
|
||||
l.RemoveMsgs[chain].Msgs[queueID] = message.RemoveListenMsg_req{
|
||||
QueueId: queueID,
|
||||
MsgType: msgType,
|
||||
Chain: chain,
|
||||
Symbol: symbol,
|
||||
Address: address,
|
||||
Timestamp: uint64(timestamp),
|
||||
Sign: sign,
|
||||
Status: status,
|
||||
}
|
||||
l.RemoveMsgs[chain].mu.RUnlock()
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Printf("error occurred during rows iteration: %v", err)
|
||||
}
|
||||
log.Printf("移除监听历史消息load完毕")
|
||||
}()
|
||||
}
|
||||
|
||||
// 充值消息
|
||||
func (l *ListenServer) handleRmqTopup_req(msg message.TopupMsg_req) {
|
||||
// 添加到TopupMsgs
|
||||
l.TopupMsgs[msg.Chain].mu.RLock()
|
||||
l.TopupMsgs[msg.Chain].Msgs[msg.QueueId] = msg
|
||||
l.TopupMsgs[msg.Chain].mu.RUnlock()
|
||||
// 写数据库
|
||||
sql := "INSERT INTO topup_req_msg (queue_id, chain, symbol, address, timestamp, sign) VALUES (?,?,?,?,?,?)"
|
||||
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Address, msg.Timestamp, msg.Sign}
|
||||
_, err := l.MysqlDB.Insert(sql, [][]any{params})
|
||||
if err != nil {
|
||||
log.Printf("Insert Topup_req msg error: %v", err)
|
||||
go l.asyncSendMsgToRmq(message.TopupMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
Address: msg.Address,
|
||||
Status: constant.STATUS_ERROR,
|
||||
}, 3, 5*time.Second)
|
||||
return
|
||||
}
|
||||
// 传给对应的node server
|
||||
go l.asyncSendMsgToChain(msg, msg.Chain, 3, 5*time.Second)
|
||||
log.Printf("Insert Topup_req msg success: QueueId(%s)", msg.QueueId)
|
||||
}
|
||||
|
||||
// 提现消息
|
||||
func (l *ListenServer) handleRmqWithdraw_req(msg message.WithdrawMsg_req) {
|
||||
// 添加到WithdrawMsgs
|
||||
l.WithdrawMsgs[msg.Chain].mu.RLock()
|
||||
l.WithdrawMsgs[msg.Chain].Msgs[msg.QueueId] = msg
|
||||
l.WithdrawMsgs[msg.Chain].mu.RUnlock()
|
||||
// 写数据库
|
||||
sql := "INSERT INTO withdraw_req_msg (queue_id, from_addr, to_addr, amount, fee, chain, symbol, timestamp, sign) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||
params := []any{msg.QueueId, msg.FromAddress, msg.ToAddress, fmt.Sprintf("%f", msg.Amount), fmt.Sprintf("%f", msg.Fee), msg.Chain, msg.Symbol, msg.Timestamp, msg.Sign}
|
||||
_, err := l.MysqlDB.Insert(sql, [][]any{params})
|
||||
if err != nil {
|
||||
log.Printf("Insert Withdraw_req msg error: %v", err)
|
||||
go l.asyncSendMsgToRmq(message.WithdrawMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
FromAddress: msg.FromAddress,
|
||||
ToAddress: msg.ToAddress,
|
||||
Status: constant.STATUS_ERROR,
|
||||
}, 3, 5*time.Second)
|
||||
return
|
||||
}
|
||||
// 传给对应的node server
|
||||
go l.asyncSendMsgToChain(msg, msg.Chain, 3, 5*time.Second)
|
||||
log.Printf("Insert Withdraw_req msg success: QueueId(%s)", msg.QueueId)
|
||||
}
|
||||
|
||||
// 支付消息
|
||||
func (l *ListenServer) handleRmqPay_req(msg message.PayMsg_req) {
|
||||
// 添加到WithdrawMsgs
|
||||
l.PayMsgs[msg.Chain].mu.RLock()
|
||||
l.PayMsgs[msg.Chain].Msgs[msg.QueueId] = msg
|
||||
l.PayMsgs[msg.Chain].mu.RUnlock()
|
||||
// 写数据库
|
||||
sql := "INSERT INTO pay_req_msg (queue_id, from_addr, to_addr, amount, fee, chain, symbol, timestamp, sign) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||
params := []any{msg.QueueId, msg.FromAddress, msg.ToAddress, fmt.Sprintf("%f", msg.Amount), fmt.Sprintf("%f", msg.Fee), msg.Chain, msg.Symbol, msg.Timestamp, msg.Sign}
|
||||
_, err := l.MysqlDB.Insert(sql, [][]any{params})
|
||||
if err != nil {
|
||||
log.Printf("Insert PayMsg_req msg error: %v", err)
|
||||
go l.asyncSendMsgToRmq(message.PayMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
FromAddress: msg.FromAddress,
|
||||
ToAddress: msg.ToAddress,
|
||||
Status: constant.STATUS_ERROR,
|
||||
}, 3, 5*time.Second)
|
||||
return
|
||||
}
|
||||
// 传给对应的node server
|
||||
go l.asyncSendMsgToChain(msg, msg.Chain, 3, 5*time.Second)
|
||||
log.Printf("Insert PayMsg_req msg success: QueueId(%s)", msg.QueueId)
|
||||
}
|
||||
|
||||
// 移除监听消息
|
||||
func (l *ListenServer) handleRmqRemove_req(msg message.RemoveListenMsg_req) {
|
||||
l.RemoveMsgs[msg.Chain].mu.RLock()
|
||||
l.RemoveMsgs[msg.Chain].Msgs[msg.QueueId] = msg
|
||||
l.RemoveMsgs[msg.Chain].mu.RUnlock()
|
||||
|
||||
sql := "INSERT INTO remove_req_msg (queue_id, msg_type, chain, symbol, address, timestamp, sign) VALUES (?,?,?,?,?,?,?)"
|
||||
params := []any{msg.QueueId, msg.MsgType, msg.Chain, msg.Symbol, msg.Address, msg.Timestamp, msg.Sign}
|
||||
_, err := l.MysqlDB.Insert(sql, [][]any{params})
|
||||
if err != nil {
|
||||
log.Printf("Insert Remove_req msg error: %v", err)
|
||||
go l.asyncSendMsgToRmq(message.RemoveListenMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
MsgType: msg.MsgType,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
Address: msg.Address,
|
||||
Status: constant.STATUS_ERROR,
|
||||
}, 3, 5*time.Second)
|
||||
}
|
||||
|
||||
go l.asyncSendMsgToChain(msg, msg.Chain, 3, 5*time.Second)
|
||||
log.Printf("Insert Remove_req msg success: QueueId(%s)", msg.QueueId)
|
||||
}
|
||||
|
||||
// 更新状态响应
|
||||
func (l *ListenServer) handleUpdateReqState(msg message.UpdateReqState) {
|
||||
typeMap := map[int]string{
|
||||
0: "topup_req_msg",
|
||||
1: "withdraw_req_msg",
|
||||
2: "pay_req_msg",
|
||||
}
|
||||
str := "UPDATE " + typeMap[msg.MsgType] + " SET status = ? WHERE queue_id = ?"
|
||||
params := []any{msg.Status, msg.QueueId}
|
||||
go func() {
|
||||
_, err := l.MysqlDB.Update(str, params)
|
||||
if err != nil {
|
||||
// 更详细的错误日志,包括 QueueId 和 Status
|
||||
log.Printf("Failed to update update_req_msg for queue_id %s: %v", msg.QueueId, err)
|
||||
}
|
||||
// else if count != 1 {
|
||||
// // 如果更新的行数不是 1,日志中记录详细信息
|
||||
// log.Printf("Unexpected update count for queue_id %s: expected 1, got %d", msg.QueueId, count)
|
||||
// }
|
||||
}()
|
||||
}
|
||||
|
||||
// 充值响应
|
||||
func (l *ListenServer) handleChainTopup_resp(msg message.TopupMsg_resp) {
|
||||
switch msg.Status {
|
||||
case constant.STATUS_SUCCESS, constant.STATUS_FAILED:
|
||||
// 修改数据库
|
||||
str := "UPDATE topup_resp_msg SET status = ? WHERE tx_hash = ?"
|
||||
params := []any{msg.Status, msg.TxHash}
|
||||
_, err := l.MysqlDB.Update(str, params)
|
||||
if err != nil {
|
||||
// 更详细的错误日志,包括 QueueId 和 Status
|
||||
log.Printf("Failed to update topup_resp_msg for queue_id %s: %v", msg.QueueId, err)
|
||||
}
|
||||
// else if count != 1 {
|
||||
// // 如果更新的行数不是 1,日志中记录详细信息
|
||||
// log.Printf("Unexpected update count for queue_id %s: expected 1, got %d", msg.QueueId, count)
|
||||
// }
|
||||
case constant.STATUS_PENDING:
|
||||
str := "INSERT INTO topup_resp_msg (queue_id, chain, symbol, from_addr, to_addr, amount, tx_hash, height, status) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.Address, msg.Amount, msg.TxHash, msg.BlockHeight, msg.Status}
|
||||
_, err := l.MysqlDB.Insert(str, [][]any{params})
|
||||
if err != nil {
|
||||
log.Printf("Insert Topup_resp msg error: %v", err)
|
||||
}
|
||||
default:
|
||||
// 插入数据库
|
||||
str := "INSERT INTO topup_resp_msg (queue_id, chain, symbol, to_addr, status) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.Address, msg.Status}
|
||||
_, err := l.MysqlDB.Insert(str, [][]any{params})
|
||||
if err != nil {
|
||||
log.Printf("Insert Topup_resp msg error: %v", err)
|
||||
}
|
||||
}
|
||||
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
|
||||
}
|
||||
|
||||
// 提现响应
|
||||
func (l *ListenServer) handleChainWithdraw_resp(msg message.WithdrawMsg_resp) {
|
||||
switch msg.Status {
|
||||
// pending状态表示转账已完成且在区块链中被找到,等待后续确认+记录到数据库
|
||||
case constant.STATUS_PENDING:
|
||||
go func() {
|
||||
str := "INSERT INTO withdraw_resp_msg (queue_id, chain, symbol, from_addr, to_addr, tx_hash, amount, fee, height, status) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, msg.TxHash, msg.Amount, msg.Fee, msg.BlockHeight, msg.Status}
|
||||
_, err := l.MysqlDB.Insert(str, [][]any{params})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
case constant.STATUS_SUCCESS, constant.STATUS_FAILED:
|
||||
l.WithdrawMsgs[msg.Chain].mu.Lock()
|
||||
delete(l.WithdrawMsgs[msg.Chain].Msgs, msg.QueueId)
|
||||
l.WithdrawMsgs[msg.Chain].mu.Unlock()
|
||||
|
||||
go func() {
|
||||
str := "UPDATE withdraw_resp_msg SET status = ? WHERE tx_hash = ?"
|
||||
params := []any{msg.Status, msg.TxHash}
|
||||
_, err := l.MysqlDB.Update(str, params)
|
||||
if err != nil {
|
||||
// 更详细的错误日志,包括 QueueId 和 Status
|
||||
log.Printf("Failed to update withdraw_resp_msg for queue_id %s: %v", msg.QueueId, err)
|
||||
}
|
||||
// else if count != 1 {
|
||||
// // 如果更新的行数不是 1,日志中记录详细信息
|
||||
// log.Printf("Unexpected update count for queue_id %s: expected 1, got %d", msg.QueueId, count)
|
||||
// }
|
||||
}()
|
||||
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
|
||||
default:
|
||||
l.WithdrawMsgs[msg.Chain].mu.Lock()
|
||||
delete(l.WithdrawMsgs[msg.Chain].Msgs, msg.QueueId)
|
||||
l.WithdrawMsgs[msg.Chain].mu.Unlock()
|
||||
|
||||
go func() {
|
||||
str := "INSERT INTO withdraw_resp_msg (queue_id, chain, symbol, from_addr, to_addr, amount, fee, status) VALUES (?,?,?,?,?,?,?,?)"
|
||||
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, fmt.Sprintf("%f", msg.Amount), fmt.Sprintf("%f", msg.Fee), msg.Status}
|
||||
_, err := l.MysqlDB.Insert(str, [][]any{params})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// 支付响应
|
||||
func (l *ListenServer) handleChainPay_resp(msg message.PayMsg_resp) {
|
||||
switch msg.Status {
|
||||
// pending状态表示转账已完成且在区块链中被找到,等待后续确认+记录到数据库
|
||||
case constant.STATUS_PENDING:
|
||||
go func() {
|
||||
str := "INSERT INTO pay_resp_msg (queue_id, chain, symbol, from_addr, to_addr, tx_hash, amount, fee, height, status) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, msg.TxHash, msg.Amount, msg.Fee, msg.BlockHeight, msg.Status}
|
||||
_, err := l.MysqlDB.Insert(str, [][]any{params})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
case constant.STATUS_SUCCESS, constant.STATUS_FAILED:
|
||||
l.WithdrawMsgs[msg.Chain].mu.Lock()
|
||||
delete(l.WithdrawMsgs[msg.Chain].Msgs, msg.QueueId)
|
||||
l.WithdrawMsgs[msg.Chain].mu.Unlock()
|
||||
|
||||
go func() {
|
||||
str := "UPDATE pay_resp_msg SET status = ? WHERE tx_hash = ?"
|
||||
params := []any{msg.Status, msg.TxHash}
|
||||
_, err := l.MysqlDB.Update(str, params)
|
||||
if err != nil {
|
||||
// 更详细的错误日志,包括 QueueId 和 Status
|
||||
log.Printf("Failed to update pay_resp_msg for queue_id %s: %v", msg.QueueId, err)
|
||||
}
|
||||
// else if count != 1 {
|
||||
// // 如果更新的行数不是 1,日志中记录详细信息
|
||||
// log.Printf("Unexpected update count for queue_id %s: expected 1, got %d", msg.QueueId, count)
|
||||
// }
|
||||
}()
|
||||
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
|
||||
default:
|
||||
l.WithdrawMsgs[msg.Chain].mu.Lock()
|
||||
delete(l.WithdrawMsgs[msg.Chain].Msgs, msg.QueueId)
|
||||
l.WithdrawMsgs[msg.Chain].mu.Unlock()
|
||||
|
||||
go func() {
|
||||
str := "INSERT INTO pay_resp_msg (queue_id, chain, symbol, from_addr, to_addr, amount, fee, status) VALUES (?,?,?,?,?,?,?,?)"
|
||||
params := []any{msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, fmt.Sprintf("%f", msg.Amount), fmt.Sprintf("%f", msg.Fee), msg.Status}
|
||||
_, err := l.MysqlDB.Insert(str, [][]any{params})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除监听响应
|
||||
func (l *ListenServer) handleChainRemove_resp(msg message.RemoveListenMsg_resp) {
|
||||
// 如果状态为成功,移除该 QueueId
|
||||
if msg.Status == constant.STATUS_SUCCESS {
|
||||
l.RemoveMsgs[msg.Chain].mu.Lock()
|
||||
delete(l.RemoveMsgs[msg.Chain].Msgs, msg.QueueId)
|
||||
l.RemoveMsgs[msg.Chain].mu.Unlock()
|
||||
}
|
||||
|
||||
// 使用 goroutine 异步更新数据库
|
||||
go func() {
|
||||
// 更新数据库
|
||||
str := "UPDATE remove_resp_msg SET status = ? WHERE queue_id = ?"
|
||||
params := []any{msg.Status, msg.QueueId}
|
||||
_, err := l.MysqlDB.Update(str, params)
|
||||
if err != nil {
|
||||
// 更详细的错误日志,包括 QueueId 和 Status
|
||||
log.Printf("Failed to update remove_resp_msg for queue_id %s: %v", msg.QueueId, err)
|
||||
}
|
||||
// else if count != 1 {
|
||||
// // 如果更新的行数不是 1,日志中记录详细信息
|
||||
// log.Printf("Unexpected update count for queue_id %s: expected 1, got %d", msg.QueueId, count)
|
||||
// }
|
||||
}()
|
||||
|
||||
// 异步发送消息到 RMQ
|
||||
go l.asyncSendMsgToRmq(msg, 3, 5*time.Second)
|
||||
}
|
||||
|
||||
// 异步发送通道消息,Listen -> Chain server
|
||||
func (l *ListenServer) asyncSendMsgToChain(msg any, chain string, retries int, timeout time.Duration) {
|
||||
for retries > 0 {
|
||||
select {
|
||||
case l.ChToChainServer[chain] <- msg: // 如果通道没有满,就发送消息
|
||||
// log.Printf("Sent message to rmq_ch_out: %v", msg)
|
||||
return
|
||||
case <-time.After(timeout): // 超时控制
|
||||
log.Printf("Timeout sending message to rmq_ch_out: %v", msg)
|
||||
retries--
|
||||
if retries == 0 {
|
||||
log.Printf("Max retries reached, giving up on sending message: %v", msg)
|
||||
return
|
||||
}
|
||||
// 在超时后进行重试
|
||||
log.Printf("Retrying sending message: %v, retries left: %d", msg, retries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 异步发送通道消息,Listen -> Rmq
|
||||
func (l *ListenServer) asyncSendMsgToRmq(msg any, retries int, timeout time.Duration) {
|
||||
for retries > 0 {
|
||||
select {
|
||||
case l.ChToRmqServer <- msg: // 如果通道没有满,就发送消息
|
||||
// log.Printf("Sent message to rmq_ch_out: %v", msg)
|
||||
return
|
||||
case <-time.After(timeout): // 超时控制
|
||||
log.Printf("Timeout sending message to rmq_ch_out: %v", msg)
|
||||
retries--
|
||||
if retries == 0 {
|
||||
log.Printf("Max retries reached, giving up on sending message: %v", msg)
|
||||
return
|
||||
}
|
||||
// 在超时后进行重试
|
||||
log.Printf("Retrying sending message: %v, retries left: %d", msg, retries)
|
||||
}
|
||||
}
|
||||
}
|
||||
41
internal/logger/README.md
Normal file
41
internal/logger/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 交易日志系统
|
||||
|
||||
## 核心功能
|
||||
|
||||
1. **按地址/订单分离日志**
|
||||
- 充值:按 `address` 命名
|
||||
- 提现:按 `queueId` 命名
|
||||
- 支付:按 `orderId` 命名
|
||||
|
||||
2. **自动日志轮转**
|
||||
- 单文件超过 1MB 自动压缩为 `.log.gz`
|
||||
- 后台异步压缩,不影响性能
|
||||
|
||||
3. **日志格式**
|
||||
```
|
||||
2024-01-01 12:00:00 [topup]-[待确认] | 金额: 100.000000 | 交易哈希: 0x123... | 区块高度: 12345678 | 地址: 0xabc...
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
```go
|
||||
// 初始化
|
||||
logger.InitTransactionLogger("logs")
|
||||
defer logger.CloseTransactionLogger()
|
||||
|
||||
// 记录日志
|
||||
logger.LogTopup(address, "待确认", amount, txHash, blockHeight)
|
||||
logger.LogWithdraw(queueId, "确认", amount, from, to, txHash, blockHeight)
|
||||
logger.LogPay(orderId, queueId, "确认", amount, from, to, txHash, blockHeight)
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
修改 `internal/logger/transaction_logger.go` 常量:
|
||||
```go
|
||||
const (
|
||||
MaxFileSize = 1 * 1024 * 1024 // 日志轮转大小
|
||||
LogDir = "logs" // 日志目录
|
||||
)
|
||||
```
|
||||
|
||||
483
internal/logger/transaction_logger.go
Normal file
483
internal/logger/transaction_logger.go
Normal file
@@ -0,0 +1,483 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxFileSize = 1 * 1024 * 1024 // 1MB
|
||||
LogDir = "logs"
|
||||
)
|
||||
|
||||
// TransactionLogger 交易日志记录器
|
||||
type TransactionLogger struct {
|
||||
mu sync.Mutex
|
||||
files map[string]*logFile // address -> logFile
|
||||
logDir string
|
||||
}
|
||||
|
||||
// logFile 单个日志文件
|
||||
type logFile struct {
|
||||
file *os.File
|
||||
size int64
|
||||
address string
|
||||
logDir string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
txLogger *TransactionLogger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// InitTransactionLogger 初始化交易日志系统
|
||||
func InitTransactionLogger(logDir string) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
if logDir == "" {
|
||||
logDir = LogDir
|
||||
}
|
||||
|
||||
// 创建日志目录
|
||||
err = os.MkdirAll(logDir, 0755)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
txLogger = &TransactionLogger{
|
||||
files: make(map[string]*logFile),
|
||||
logDir: logDir,
|
||||
}
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// getOrCreateLogFile 获取或创建日志文件
|
||||
func (tl *TransactionLogger) getOrCreateLogFile(address string) (*logFile, error) {
|
||||
tl.mu.Lock()
|
||||
defer tl.mu.Unlock()
|
||||
|
||||
// 如果已存在,返回现有的
|
||||
if lf, exists := tl.files[address]; exists {
|
||||
return lf, nil
|
||||
}
|
||||
|
||||
// 创建新的日志文件
|
||||
lf := &logFile{
|
||||
address: address,
|
||||
logDir: tl.logDir,
|
||||
}
|
||||
|
||||
// 打开或创建文件
|
||||
filePath := filepath.Join(tl.logDir, fmt.Sprintf("%s.log", address))
|
||||
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建日志文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取当前文件大小
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, fmt.Errorf("获取文件信息失败: %w", err)
|
||||
}
|
||||
|
||||
lf.file = file
|
||||
lf.size = info.Size()
|
||||
tl.files[address] = lf
|
||||
|
||||
return lf, nil
|
||||
}
|
||||
|
||||
// write 写入日志
|
||||
func (lf *logFile) write(content string) error {
|
||||
lf.mu.Lock()
|
||||
defer lf.mu.Unlock()
|
||||
|
||||
// 检查是否需要轮转
|
||||
if lf.size >= MaxFileSize {
|
||||
if err := lf.rotate(); err != nil {
|
||||
return fmt.Errorf("日志轮转失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 写入内容
|
||||
n, err := lf.file.WriteString(content + "\n")
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入日志失败: %w", err)
|
||||
}
|
||||
|
||||
lf.size += int64(n)
|
||||
|
||||
// 立即刷新到磁盘
|
||||
lf.file.Sync()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// rotate 日志轮转:压缩当前文件,创建新文件
|
||||
func (lf *logFile) rotate() error {
|
||||
// 关闭当前文件
|
||||
if lf.file != nil {
|
||||
lf.file.Close()
|
||||
}
|
||||
|
||||
// 生成备份文件名(带时间戳)
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
oldPath := filepath.Join(lf.logDir, fmt.Sprintf("%s.log", lf.address))
|
||||
backupPath := filepath.Join(lf.logDir, fmt.Sprintf("%s_%s.log", lf.address, timestamp))
|
||||
|
||||
// 重命名当前文件
|
||||
if err := os.Rename(oldPath, backupPath); err != nil {
|
||||
return fmt.Errorf("重命名日志文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 压缩备份文件
|
||||
go func() {
|
||||
if err := compressFile(backupPath); err != nil {
|
||||
fmt.Printf("⚠️ 压缩日志文件失败 %s: %v\n", backupPath, err)
|
||||
} else {
|
||||
// 删除原文件
|
||||
os.Remove(backupPath)
|
||||
}
|
||||
}()
|
||||
|
||||
// 创建新文件
|
||||
file, err := os.OpenFile(oldPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建新日志文件失败: %w", err)
|
||||
}
|
||||
|
||||
lf.file = file
|
||||
lf.size = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// compressFile 压缩文件
|
||||
func compressFile(filePath string) error {
|
||||
// 打开原文件
|
||||
srcFile, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// 创建压缩文件
|
||||
gzPath := filePath + ".gz"
|
||||
gzFile, err := os.Create(gzPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gzFile.Close()
|
||||
|
||||
// 创建 gzip writer
|
||||
gzWriter := gzip.NewWriter(gzFile)
|
||||
defer gzWriter.Close()
|
||||
|
||||
// 复制数据
|
||||
_, err = io.Copy(gzWriter, srcFile)
|
||||
return err
|
||||
}
|
||||
|
||||
// LogTopup 记录充值消息
|
||||
func LogTopup(toAddress string, status string, amount float64, txHash string, blockHeight uint64) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
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 | ToAddress: %s",
|
||||
timestamp, status, amount, txHash, blockHeight, toAddress)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithdraw 记录提现消息
|
||||
func LogWithdraw(fromAddress string, status string, amount float64, toAddress string, txHash string, blockHeight uint64) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 toAddress 作为文件名
|
||||
lf, err := txLogger.getOrCreateLogFile(fromAddress)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
content := fmt.Sprintf("%s [提现]-[%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)
|
||||
}
|
||||
}
|
||||
|
||||
// LogPay 记录支付消息
|
||||
func LogPay(status string, fromAddress string, queueId string) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 toAddress 作为文件名
|
||||
lf, err := txLogger.getOrCreateLogFile(fromAddress)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
// t, err := json.Marshal(transactions)
|
||||
// if err != nil {
|
||||
// fmt.Println("Error marshalling to JSON:", err)
|
||||
// return
|
||||
// }
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
content := fmt.Sprintf("%s [pay]-[%s] | FromAddress: %s | QueueId: %s",
|
||||
timestamp, status, fromAddress, queueId)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 记录当前监听的钱包和所有消息
|
||||
func LogETHNode(msg string) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lf, err := txLogger.getOrCreateLogFile("ethnode")
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
content := fmt.Sprintf("[ETH-NODE:%s]: %s", timestamp, msg)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== RMQ <-> Listen 通信日志 ===============================
|
||||
|
||||
// LogRmqToListenTopupReq 记录 RMQ -> Listen 充值请求
|
||||
// 使用 address 作为文件名
|
||||
func LogRmqToListenTopupReq(queueId, chain, symbol, address string, timestamp uint64) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lf, err := txLogger.getOrCreateLogFile(address)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
t := time.Now().Format("2006-01-02 15:04:05")
|
||||
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
|
||||
// TopupReq 没有 fromAddress,使用 "-" 代替,toAddress 是 address,amount 为 0
|
||||
content := fmt.Sprintf("[TopupReq]: %s--%s-%s-%s-0",
|
||||
t, address, chain, symbol)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogRmqToListenWithdrawReq 记录 RMQ -> Listen 提现请求
|
||||
// 使用 fromAddress 作为文件名
|
||||
func LogRmqToListenWithdrawReq(queueId, chain, symbol, fromAddress, toAddress string, amount, fee float64, timestamp uint64) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lf, err := txLogger.getOrCreateLogFile(fromAddress)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
t := time.Now().Format("2006-01-02 15:04:05")
|
||||
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
|
||||
content := fmt.Sprintf("[WithdrawReq]: %s-%s-%s-%s-%s-%.6f",
|
||||
t, fromAddress, toAddress, chain, symbol, amount)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogRmqToListenPayReq 记录 RMQ -> Listen 支付请求
|
||||
// 使用 fromAddress 作为文件名
|
||||
func LogRmqToListenPayReq(queueId, chain, symbol, fromAddress, toAddress string, amount, fee float64, timestamp uint64) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lf, err := txLogger.getOrCreateLogFile(fromAddress)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
t := time.Now().Format("2006-01-02 15:04:05")
|
||||
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
|
||||
content := fmt.Sprintf("[PayReq]: %s-%s-%s-%s-%s-%.6f",
|
||||
t, fromAddress, toAddress, chain, symbol, amount)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogRmqToListenRemoveReq 记录 RMQ -> Listen 移除监听请求
|
||||
// 使用 address 作为文件名
|
||||
func LogRmqToListenRemoveReq(queueId string, msgType int, chain, symbol, address string, timestamp uint64) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lf, err := txLogger.getOrCreateLogFile(address)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
t := time.Now().Format("2006-01-02 15:04:05")
|
||||
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
|
||||
// RemoveReq 没有 fromAddress,toAddress 是 address,amount 为 0
|
||||
content := fmt.Sprintf("[RemoveReq]: %s--%s-%s-%s-0",
|
||||
t, address, chain, symbol)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogListenToRmqTopupResp 记录 Listen -> RMQ 充值响应
|
||||
// 使用 address 作为文件名
|
||||
func LogListenToRmqTopupResp(queueId, chain, symbol, address, fromAddress, txHash string, amount float64, blockHeight uint64, status int) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lf, err := txLogger.getOrCreateLogFile(address)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
t := time.Now().Format("2006-01-02 15:04:05")
|
||||
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
|
||||
// TopupResp 中 address 是目标地址(toAddress),fromAddress 是来源地址
|
||||
content := fmt.Sprintf("[TopupResp]: %s-%s-%s-%s-%s-%.6f",
|
||||
t, fromAddress, address, chain, symbol, amount)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogListenToRmqWithdrawResp 记录 Listen -> RMQ 提现响应
|
||||
// 使用 fromAddress 作为文件名
|
||||
func LogListenToRmqWithdrawResp(queueId, chain, symbol, fromAddress, toAddress, txHash string, amount, fee float64, blockHeight uint64, status int) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lf, err := txLogger.getOrCreateLogFile(fromAddress)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
t := time.Now().Format("2006-01-02 15:04:05")
|
||||
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
|
||||
content := fmt.Sprintf("[WithdrawResp]: %s-%s-%s-%s-%s-%.6f",
|
||||
t, fromAddress, toAddress, chain, symbol, amount)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogListenToRmqPayResp 记录 Listen -> RMQ 支付响应
|
||||
// 使用 fromAddress 作为文件名
|
||||
func LogListenToRmqPayResp(queueId, chain, symbol, fromAddress, toAddress, txHash string, amount, fee float64, blockHeight uint64, status int) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lf, err := txLogger.getOrCreateLogFile(fromAddress)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
t := time.Now().Format("2006-01-02 15:04:05")
|
||||
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
|
||||
content := fmt.Sprintf("[PayResp]: %s-%s-%s-%s-%s-%.6f",
|
||||
t, fromAddress, toAddress, chain, symbol, amount)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogListenToRmqRemoveResp 记录 Listen -> RMQ 移除监听响应
|
||||
// 使用 address 作为文件名
|
||||
func LogListenToRmqRemoveResp(queueId string, msgType int, chain, symbol, address string, status int) {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lf, err := txLogger.getOrCreateLogFile(address)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ 获取日志文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
t := time.Now().Format("2006-01-02 15:04:05")
|
||||
// 格式:[msg-Type]: time-fromaddress-toaddress-chain-symbol-amount
|
||||
// RemoveResp 没有 fromAddress,toAddress 是 address,amount 为 0
|
||||
content := fmt.Sprintf("[RemoveResp]: %s--%s-%s-%s-0",
|
||||
t, address, chain, symbol)
|
||||
|
||||
if err := lf.write(content); err != nil {
|
||||
fmt.Printf("⚠️ 写入日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭所有日志文件
|
||||
func CloseTransactionLogger() {
|
||||
if txLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
txLogger.mu.Lock()
|
||||
defer txLogger.mu.Unlock()
|
||||
|
||||
for _, lf := range txLogger.files {
|
||||
if lf.file != nil {
|
||||
lf.file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
53
internal/msg/config.go
Normal file
53
internal/msg/config.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package msg
|
||||
|
||||
import "time"
|
||||
|
||||
// 配置文件结构
|
||||
type Config struct {
|
||||
Net []string `json:"net"`
|
||||
RmqConfig RmqConfig `json:"rmq_config"`
|
||||
MsgConfig MsgConfig `json:"msg_config"`
|
||||
MysqlConfig map[string]MysqlConfig `json:"mysql_config"` // {dbname: MysqlConfig{}, ...}
|
||||
ETHConfig ETHConfig `json:"eth_config"`
|
||||
}
|
||||
|
||||
type RmqConfig struct {
|
||||
SubAddr string `json:"sub_addr"`
|
||||
Pay Queue `json:"pay"`
|
||||
Topup Queue `json:"topup"`
|
||||
Withdraw Queue `json:"withdraw"`
|
||||
Remove Queue `json:"remove"`
|
||||
PayResp Queue `json:"pay_resp"`
|
||||
TopupResp Queue `json:"topup_resp"`
|
||||
WithdrawResp Queue `json:"withdraw_resp"`
|
||||
RemoveResp Queue `json:"remove_resp"`
|
||||
}
|
||||
|
||||
type Queue struct {
|
||||
Queue string `json:"queue"`
|
||||
Exchange string `json:"exchange"`
|
||||
Routing []string `json:"routing"`
|
||||
}
|
||||
|
||||
type MsgConfig struct {
|
||||
SqlitePath string `json:"sqlite_path"` // sqlite3数据库文件路径
|
||||
}
|
||||
|
||||
type MysqlConfig struct {
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Database string `json:"database"`
|
||||
MaxOpenConns int `json:"maxOpenConns"`
|
||||
MaxIdleConns int `json:"maxIdleConns"`
|
||||
ConnMaxLife time.Duration `json:"connMaxLife"`
|
||||
}
|
||||
|
||||
type ETHConfig struct {
|
||||
RpcUrl string `json:"rpc_url"`
|
||||
WsUrl string `json:"ws_url"`
|
||||
ConfirmHeight uint64 `json:"confirm_height"`
|
||||
SqlitePath string `json:"sqlite_path"` // sqlite3数据库文件路径
|
||||
SupportTokens []string `json:"support_tokens"` // 支持的代币列表,如["USDT", "DAI"]
|
||||
}
|
||||
@@ -1,132 +1,188 @@
|
||||
package msg
|
||||
|
||||
import "time"
|
||||
import "math/big"
|
||||
|
||||
// 配置文件结构
|
||||
type Config struct {
|
||||
RMQConfig RMQConfig `json:"rmq_config"`
|
||||
ETHConfig ETHConfig `json:"eth_config"`
|
||||
TRONConfig TRONConfig `json:"tron_config"`
|
||||
}
|
||||
|
||||
type RMQConfig struct {
|
||||
SubAddr string `json:"sub_addr"` // 监听地址
|
||||
PayConfig QueueConfig `json:"pay"` // 支付
|
||||
TopUpConfig QueueConfig `json:"topup"` // 充值
|
||||
WithdrawConfig QueueConfig `json:"withdraw"` // 提现
|
||||
PayRespConfig QueueConfig `json:"pay_resp"` // 支付回复
|
||||
TopUpRespConfig QueueConfig `json:"topup_resp"` // 充值回复
|
||||
WithdrawRespConfig QueueConfig `json:"withdraw_resp"` // 提现回复
|
||||
}
|
||||
|
||||
type QueueConfig struct {
|
||||
QueueName string `json:"queue"`
|
||||
ExchangeName string `json:"exchange"`
|
||||
Routing []string `json:"routing"`
|
||||
}
|
||||
|
||||
type ETHConfig struct {
|
||||
RpcURL string `json:"rpcUrl"` // rpc连接地址
|
||||
WsURL string `json:"wsUrl"` // websocket连接地址
|
||||
ConfirmHeight uint64 `json:"confirmHeight"` // 确认所需区块数
|
||||
DbConfig DbConfig `json:"dbConfig"`
|
||||
}
|
||||
|
||||
type TRONConfig struct {
|
||||
RpcUrl string `json:"rpcUrl"`
|
||||
ConfirmHeight uint64 `json:"confirmHeight"`
|
||||
DbConfig DbConfig `json:"dbConfig"`
|
||||
}
|
||||
|
||||
// Config 数据库配置
|
||||
type DbConfig struct {
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Database string `json:"database"`
|
||||
MaxOpenConns int `json:"maxOpenConns"` // 最大打开连接数
|
||||
MaxIdleConns int `json:"maxIdleCoons"` // 最大空闲连接数
|
||||
ConnMaxLife time.Duration `json:"connMaxLife"` // 连接最大存活时间
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// =============================== type0 ===============================
|
||||
// 接收的充值消息
|
||||
type TopupMsg_req struct {
|
||||
QueueId string `json:"queue_id"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
Address string `json:"address"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
Status int `json:"status,omitempty"` // 1监听中,2停止监听
|
||||
}
|
||||
|
||||
// 返回充值结果消息
|
||||
type TopupMsg_resp struct {
|
||||
Address string `json:"address"`
|
||||
Status int `json:"status"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
Amount float64 `json:"amount"`
|
||||
TxHash string `json:"tx_hash"`
|
||||
QueueId string `json:"queue_id"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
FromAddress string `json:"from_address,omitempty"` // 充值来源地址
|
||||
Address string `json:"address"` // 充值目标地址
|
||||
TxHash string `json:"tx_hash,omitempty"`
|
||||
Amount float64 `json:"amount,omitempty"`
|
||||
BlockHeight uint64 `json:"block_height,omitempty"` // 区块高度
|
||||
Status int `json:"status"` // 0失败,1成功,2待定,3sign校验失败
|
||||
}
|
||||
|
||||
// =============================== type1 ===============================
|
||||
// 接收的提现消息
|
||||
type WithdrawMsg_req struct {
|
||||
QueueId string `json:"queue_id"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
FromAddress string `json:"from_address"` // 我们提供的地址
|
||||
ToAddress string `json:"to_address"` // 用户要提现到的地址
|
||||
Amount float64 `json:"amount"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
Fee float64 `json:"fee"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
Status int `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// 返回提现结果消息
|
||||
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"` // 币种
|
||||
TxHash string `json:"tx_hash"`
|
||||
QueueId string `json:"queue_id"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
FromAddress string `json:"from_address"` // 来源地址
|
||||
ToAddress string `json:"to_address"` // 目标地址
|
||||
TxHash string `json:"tx_hash,omitempty"`
|
||||
Amount float64 `json:"amount,omitempty"`
|
||||
Fee float64 `json:"fee,omitempty"`
|
||||
BlockHeight uint64 `json:"block_height,omitempty"` // 区块高度
|
||||
Status int `json:"status"` // 0失败,1成功,3sign校验失败
|
||||
}
|
||||
|
||||
// 接收到的支付消息
|
||||
// =============================== type2 ===============================
|
||||
type PayMsg_req struct {
|
||||
QueueId string `json:"queue_id"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
FromAddress string `json:"from_address"` // 我们提供的地址
|
||||
ToAddress string `json:"to_address"` // 卖家地址
|
||||
ToAddress string `json:"to_address"` // 用户要提现到的地址
|
||||
Amount float64 `json:"amount"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
OrderId string `json:"order_id"` // 订单号
|
||||
Fee float64 `json:"fee"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
Status int `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// 返回支付结果消息
|
||||
type PayMsg_resp struct {
|
||||
QueueId string `json:"queue_id"`
|
||||
Status int `json:"status"`
|
||||
Amount float64 `json:"amount"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
OrderId string `json:"order_id"` // 订单号
|
||||
TxHash string `json:"tx_hash"`
|
||||
QueueId string `json:"queue_id"`
|
||||
Chain string `json:"chain"` // 链名称
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
FromAddress string `json:"from_address"` // 来源地址
|
||||
ToAddress string `json:"to_address"` // 目标地址
|
||||
TxHash string `json:"tx_hash,omitempty"`
|
||||
Amount float64 `json:"amount,omitempty"`
|
||||
Fee float64 `json:"fee,omitempty"`
|
||||
BlockHeight uint64 `json:"block_height,omitempty"` // 区块高度
|
||||
Status int `json:"status"` // 0失败,1成功,3sign校验失败
|
||||
}
|
||||
|
||||
// 节点通用消息结构
|
||||
type Tx_msg struct {
|
||||
TxType int `json:"tx_type"` // 转账类型:0充值,1提现,2支付
|
||||
Tx Tx `json:"tx"`
|
||||
// type PayMsg_req struct {
|
||||
// QueueId string `json:"queue_id"`
|
||||
// Chain string `json:"chain"`
|
||||
// Symbol string `json:"symbol"`
|
||||
// FromAddress string `json:"from_address"`
|
||||
// TotalAmount float64 `json:"total_amount"`
|
||||
// TotalFee float64 `json:"total_fee"`
|
||||
// Timestamp uint64 `json:"timestamp"`
|
||||
// Sign string `json:"sign"`
|
||||
// Transactions map[string]PayData `json:"transactions"` // {"to_address": PayData_req{}, ...}
|
||||
// Status int `json:"status,omitempty"`
|
||||
// }
|
||||
|
||||
// type PayMsg_resp struct {
|
||||
// QueueId string `json:"queue_id"`
|
||||
// Chain string `json:"chain"`
|
||||
// Symbol string `json:"symbol"`
|
||||
// FromAddress string `json:"from_address"`
|
||||
// PayStatus int `json:"pay_status"` // 1至少有一笔转账成功,3sign校验失败,4钱包余额不足
|
||||
// Transactions map[string]PayData `json:"transactions"` // {"to_address": PayData_resp{}, ...}
|
||||
// }
|
||||
|
||||
// type PayData struct {
|
||||
// QueueId string `json:"queue_id,omitempty"`
|
||||
// Chain string `json:"chain,omitempty"`
|
||||
// Symbol string `json:"symbol,omitempty"`
|
||||
// TxHash string `json:"tx_hash,omitempty"`
|
||||
// ToAddress string `json:"to_address"`
|
||||
// Amount float64 `json:"amount"`
|
||||
// Fee float64 `json:"fee"`
|
||||
// BlockHeight uint64 `json:"block_height,omitempty"`
|
||||
// Status int `json:"status,omitempty"` // 0失败,1成功, 2待确认
|
||||
// }
|
||||
|
||||
// =============================== type3 ===============================
|
||||
// 接收到的删除监听地址消息
|
||||
type RemoveListenMsg_req struct {
|
||||
QueueId string `json:"queue_id"`
|
||||
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"`
|
||||
Status int `json:"status,omitempty"`
|
||||
}
|
||||
type Tx struct {
|
||||
From string `json:"from"` // 充值/提现/支付的来源地址
|
||||
To string `json:"to"` // 充值/提现/支付的目标地址
|
||||
Height uint64 `json:"height"` // 区块高度
|
||||
TxHash string `json:"tx_hash"` // 交易哈希
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
Value float64 `json:"value"` // 数量,单位是币
|
||||
Status int `json:"status"` // 交易状态,1成功,0失败, 2待确认
|
||||
|
||||
// 返回收到的删除监听地址消息
|
||||
type RemoveListenMsg_resp struct {
|
||||
QueueId string `json:"queue_id"`
|
||||
MsgType int `json:"msg_type"`
|
||||
Chain string `json:"chain"`
|
||||
Symbol string `json:"symbol"`
|
||||
Address string `json:"address"`
|
||||
Status int `json:"status"` // 0失败 1成功
|
||||
}
|
||||
|
||||
// =============================== ChainServer -> ListenServer ===============================
|
||||
// 节点服务响应
|
||||
type ChainServer_resp struct {
|
||||
QueueId string
|
||||
MsgType int
|
||||
Chain string
|
||||
Symbol string
|
||||
Status int // 遵循constant模块定义
|
||||
}
|
||||
|
||||
// =============================== 其他 ===============================
|
||||
type Transaction struct {
|
||||
QueueId string `json:"queue_id,omitempty"` // 交易对应的QueueId
|
||||
// 交易对应的消息类型,0充值 1提现 2支付
|
||||
// 充值:tx.to_address = msg.Address
|
||||
// 提现/支付:tx.to_address = msg.ToAddress && tx.from_address = msg.FromAddress && tx.value = msg.Amount
|
||||
// 同时还要通过msg确认属于哪种类型,即上述3个条件是否都存在于某个msg钟
|
||||
TxType int `json:"tx_type,omitempty"`
|
||||
Chain string `json:"chain"`
|
||||
Symbol string `json:"symbol"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
TxHash string `json:"tx_hash,omitempty"`
|
||||
Height uint64 `json:"height,omitempty"`
|
||||
Amount *big.Int `json:"amount"`
|
||||
GasUsed *big.Int `json:"gas_used,omitempty"` // 转账实际产生的eth消耗
|
||||
FreezeFee *big.Int `json:"freeze_fee,omitempty"` // erc20转账冻结的usdt,只有提现才会修改
|
||||
Status int `json:"status"` // 交易状态,1成功,0失败, 2待确认
|
||||
}
|
||||
|
||||
type TransferResult struct {
|
||||
QueueId string
|
||||
MsgType int // 1提现,2支付
|
||||
Chain string
|
||||
Symbol string
|
||||
FromAddress string
|
||||
ToAddress string
|
||||
Amount float64
|
||||
Status int
|
||||
}
|
||||
|
||||
type UpdateReqState struct {
|
||||
QueueId string
|
||||
MsgType int
|
||||
Status int
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# RabbitMQ 服务模块
|
||||
|
||||
本模块提供 RabbitMQ 消息队列的封装,用于接收业务系统的请求和发送交易确认响应。
|
||||
|
||||
## 快速使用
|
||||
|
||||
```go
|
||||
// 创建 RabbitMQ 服务
|
||||
rmqServer, err := rmq.NewRabbitMQServer(config.RMQConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer rmqServer.Close()
|
||||
|
||||
// 设置消息回调
|
||||
rmqServer.OnTopupMsg = func(msg message.TopupMsg_req) {
|
||||
// 处理充值请求
|
||||
}
|
||||
|
||||
// 启动监听
|
||||
rmqServer.Start()
|
||||
|
||||
// 发送响应
|
||||
rmqServer.PublishTopupResp(response)
|
||||
```
|
||||
|
||||
## 消息队列结构
|
||||
|
||||
### 请求队列(消费)
|
||||
- `topup` - 充值请求
|
||||
- `withdraw` - 提现请求
|
||||
- `pay` - 支付请求
|
||||
|
||||
### 响应队列(发布)
|
||||
- `topup_resp` - 充值响应
|
||||
- `withdraw_resp` - 提现响应
|
||||
- `pay_resp` - 支付响应
|
||||
|
||||
## 特性
|
||||
|
||||
✅ **自动重连** - 连接断开时自动重连
|
||||
✅ **消息持久化** - 消息不会丢失
|
||||
✅ **手动确认** - 处理成功后才确认消息
|
||||
✅ **并发安全** - 支持多 goroutine 并发发布
|
||||
|
||||
更多详情请参考 [主 README](../../README.md)。
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
// RabbitMQServer RabbitMQ 服务
|
||||
type RabbitMQServer struct {
|
||||
config message.RMQConfig
|
||||
config message.RmqConfig
|
||||
conn *amqp.Connection
|
||||
channel *amqp.Channel
|
||||
mu sync.Mutex
|
||||
@@ -22,13 +22,14 @@ type RabbitMQServer struct {
|
||||
cancel context.CancelFunc
|
||||
|
||||
// 消息处理回调函数
|
||||
OnTopupMsg func(message.TopupMsg_req) // 充值请求回调
|
||||
OnWithdrawMsg func(message.WithdrawMsg_req) // 提现请求回调
|
||||
OnPayMsg func(message.PayMsg_req) // 支付请求回调
|
||||
OnTopupMsg func(message.TopupMsg_req) // 充值请求回调
|
||||
OnWithdrawMsg func(message.WithdrawMsg_req) // 提现请求回调
|
||||
OnPayMsg func(message.PayMsg_req) // 支付请求回调
|
||||
OnRemoveMsg func(message.RemoveListenMsg_req) // 删除充值监听回调
|
||||
}
|
||||
|
||||
// NewRabbitMQServer 创建 RabbitMQ 服务
|
||||
func NewRabbitMQServer(config message.RMQConfig) (*RabbitMQServer, error) {
|
||||
func NewRabbitMQServer(config message.RmqConfig) (*RabbitMQServer, error) {
|
||||
// 创建连接
|
||||
conn, err := amqp.Dial(config.SubAddr)
|
||||
if err != nil {
|
||||
@@ -58,61 +59,63 @@ func NewRabbitMQServer(config message.RMQConfig) (*RabbitMQServer, error) {
|
||||
server.Close()
|
||||
return nil, fmt.Errorf("failed to setup queues: %w", err)
|
||||
}
|
||||
|
||||
log.Println("✅ RabbitMQ队列已启动")
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// setupQueuesAndExchanges 设置队列和交换机
|
||||
func (r *RabbitMQServer) setupQueuesAndExchanges() error {
|
||||
configs := []message.QueueConfig{
|
||||
r.config.PayConfig,
|
||||
r.config.TopUpConfig,
|
||||
r.config.WithdrawConfig,
|
||||
r.config.PayRespConfig,
|
||||
r.config.TopUpRespConfig,
|
||||
r.config.WithdrawRespConfig,
|
||||
configs := []message.Queue{
|
||||
r.config.Pay,
|
||||
r.config.Topup,
|
||||
r.config.Withdraw,
|
||||
r.config.Remove,
|
||||
r.config.PayResp,
|
||||
r.config.TopupResp,
|
||||
r.config.WithdrawResp,
|
||||
r.config.RemoveResp,
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
// 声明交换机
|
||||
err := r.channel.ExchangeDeclare(
|
||||
cfg.ExchangeName, // 交换机名称
|
||||
"direct", // 类型:direct(与现有交换机类型一致)
|
||||
true, // durable
|
||||
false, // auto-deleted
|
||||
false, // internal
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
cfg.Exchange, // 交换机名称
|
||||
"direct", // 类型:direct(与现有交换机类型一致)
|
||||
true, // durable
|
||||
false, // auto-deleted
|
||||
false, // internal
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to declare exchange %s: %w", cfg.ExchangeName, err)
|
||||
return fmt.Errorf("failed to declare exchange %s: %w", cfg.Exchange, err)
|
||||
}
|
||||
|
||||
// 声明队列
|
||||
_, err = r.channel.QueueDeclare(
|
||||
cfg.QueueName, // 队列名称
|
||||
true, // durable
|
||||
false, // delete when unused
|
||||
false, // exclusive
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
cfg.Exchange, // 队列名称
|
||||
true, // durable
|
||||
false, // delete when unused
|
||||
false, // exclusive
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to declare queue %s: %w", cfg.QueueName, err)
|
||||
return fmt.Errorf("failed to declare queue %s: %w", cfg.Exchange, err)
|
||||
}
|
||||
|
||||
// 绑定队列到交换机
|
||||
for _, routingKey := range cfg.Routing {
|
||||
err = r.channel.QueueBind(
|
||||
cfg.QueueName, // 队列名称
|
||||
routingKey, // routing key
|
||||
cfg.ExchangeName, // 交换机名称
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
cfg.Queue, // 队列名称
|
||||
routingKey, // routing key
|
||||
cfg.Exchange, // 交换机名称
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to bind queue %s to exchange %s with key %s: %w",
|
||||
cfg.QueueName, cfg.ExchangeName, routingKey, err)
|
||||
cfg.Queue, cfg.Exchange, routingKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +134,8 @@ func (r *RabbitMQServer) Start() error {
|
||||
go r.consumeWithdraw()
|
||||
// 启动支付消息监听
|
||||
go r.consumePay()
|
||||
// 启动删除充值监听
|
||||
go r.consumeRemove()
|
||||
|
||||
// log.Println("🚀 RabbitMQ 服务启动成功,开始监听消息...")
|
||||
return nil
|
||||
@@ -139,8 +144,7 @@ func (r *RabbitMQServer) Start() error {
|
||||
// consumeTopup 消费充值消息
|
||||
func (r *RabbitMQServer) consumeTopup() {
|
||||
r.consumeQueue(
|
||||
r.config.TopUpConfig.QueueName,
|
||||
"topup",
|
||||
r.config.Topup.Queue,
|
||||
func(body []byte) error {
|
||||
var msg message.TopupMsg_req
|
||||
if err := json.Unmarshal(body, &msg); err != nil {
|
||||
@@ -160,8 +164,7 @@ func (r *RabbitMQServer) consumeTopup() {
|
||||
// consumeWithdraw 消费提现消息
|
||||
func (r *RabbitMQServer) consumeWithdraw() {
|
||||
r.consumeQueue(
|
||||
r.config.WithdrawConfig.QueueName,
|
||||
"withdraw",
|
||||
r.config.Withdraw.Queue,
|
||||
func(body []byte) error {
|
||||
var msg message.WithdrawMsg_req
|
||||
if err := json.Unmarshal(body, &msg); err != nil {
|
||||
@@ -181,15 +184,14 @@ func (r *RabbitMQServer) consumeWithdraw() {
|
||||
// consumePay 消费支付消息
|
||||
func (r *RabbitMQServer) consumePay() {
|
||||
r.consumeQueue(
|
||||
r.config.PayConfig.QueueName,
|
||||
"pay",
|
||||
r.config.Pay.Queue,
|
||||
func(body []byte) error {
|
||||
var msg message.PayMsg_req
|
||||
if err := json.Unmarshal(body, &msg); err != nil {
|
||||
return fmt.Errorf("failed to parse pay message: %w", err)
|
||||
}
|
||||
log.Printf("📥 [RMQ] 收到支付请求: QueueId=%s, OrderId=%s, From=%s, To=%s, Amount=%.2f %s",
|
||||
msg.QueueId, msg.OrderId, msg.FromAddress, msg.ToAddress, msg.Amount, msg.Symbol)
|
||||
log.Printf("📥 [RMQ] 收到支付请求: QueueId=%s, From=%s, To=%s, Chain=%s, Symbol=%s, Amount=%f",
|
||||
msg.QueueId, msg.FromAddress, msg.ToAddress, msg.Chain, msg.Symbol, msg.Amount)
|
||||
|
||||
if r.OnPayMsg != nil {
|
||||
r.OnPayMsg(msg)
|
||||
@@ -199,8 +201,27 @@ func (r *RabbitMQServer) consumePay() {
|
||||
)
|
||||
}
|
||||
|
||||
// consumeRemove 消费删除充值监听消息
|
||||
func (r *RabbitMQServer) consumeRemove() {
|
||||
r.consumeQueue(
|
||||
r.config.Remove.Queue,
|
||||
func(body []byte) error {
|
||||
var msg message.RemoveListenMsg_req
|
||||
if err := json.Unmarshal(body, &msg); err != nil {
|
||||
return fmt.Errorf("failed to parse remove message: %w", err)
|
||||
}
|
||||
log.Printf("📥 [RMQ] 收到删除充值监听: Chain=%s, Symbol=%s, Address=%s", msg.Chain, msg.Symbol, msg.Address)
|
||||
|
||||
if r.OnRemoveMsg != nil {
|
||||
r.OnRemoveMsg(msg)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// consumeQueue 通用队列消费方法
|
||||
func (r *RabbitMQServer) consumeQueue(queueName, msgType string, handler func([]byte) error) {
|
||||
func (r *RabbitMQServer) consumeQueue(queueName string, handler func([]byte) error) {
|
||||
for {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
@@ -246,7 +267,7 @@ func (r *RabbitMQServer) consumeQueue(queueName, msgType string, handler func([]
|
||||
// PublishTopupResp 发布充值响应
|
||||
func (r *RabbitMQServer) PublishTopupResp(resp message.TopupMsg_resp) error {
|
||||
return r.publishMessage(
|
||||
r.config.TopUpRespConfig,
|
||||
r.config.TopupResp,
|
||||
resp,
|
||||
fmt.Sprintf("充值响应: Address=%s, Status=%d, TxHash=%s",
|
||||
resp.Address, resp.Status, resp.TxHash),
|
||||
@@ -256,7 +277,7 @@ func (r *RabbitMQServer) PublishTopupResp(resp message.TopupMsg_resp) error {
|
||||
// PublishWithdrawResp 发布提现响应
|
||||
func (r *RabbitMQServer) PublishWithdrawResp(resp message.WithdrawMsg_resp) error {
|
||||
return r.publishMessage(
|
||||
r.config.WithdrawRespConfig,
|
||||
r.config.WithdrawResp,
|
||||
resp,
|
||||
fmt.Sprintf("提现响应: QueueId=%s, Status=%d, TxHash=%s",
|
||||
resp.QueueId, resp.Status, resp.TxHash),
|
||||
@@ -266,15 +287,23 @@ func (r *RabbitMQServer) PublishWithdrawResp(resp message.WithdrawMsg_resp) erro
|
||||
// PublishPayResp 发布支付响应
|
||||
func (r *RabbitMQServer) PublishPayResp(resp message.PayMsg_resp) error {
|
||||
return r.publishMessage(
|
||||
r.config.PayRespConfig,
|
||||
r.config.PayResp,
|
||||
resp,
|
||||
fmt.Sprintf("支付响应: QueueId=%s, OrderId=%s, Status=%d, TxHash=%s",
|
||||
resp.QueueId, resp.OrderId, resp.Status, resp.TxHash),
|
||||
"支付响应",
|
||||
)
|
||||
}
|
||||
|
||||
// PublishRemoveResp 发布删除充值监听响应
|
||||
func (r *RabbitMQServer) PublishRemoveResp(resp message.RemoveListenMsg_resp) error {
|
||||
return r.publishMessage(
|
||||
r.config.RemoveResp,
|
||||
resp,
|
||||
fmt.Sprintf("删除充值监听响应: Address=%s, Status=%d", resp.Address, resp.Status),
|
||||
)
|
||||
}
|
||||
|
||||
// publishMessage 通用消息发布方法
|
||||
func (r *RabbitMQServer) publishMessage(config message.QueueConfig, msg interface{}, logMsg string) error {
|
||||
func (r *RabbitMQServer) publishMessage(config message.Queue, msg interface{}, logMsg string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@@ -296,10 +325,10 @@ func (r *RabbitMQServer) publishMessage(config message.QueueConfig, msg interfac
|
||||
|
||||
err = r.channel.PublishWithContext(
|
||||
ctx,
|
||||
config.ExchangeName, // 交换机
|
||||
routingKey, // routing key
|
||||
false, // mandatory
|
||||
false, // immediate
|
||||
config.Exchange, // 交换机
|
||||
routingKey, // routing key
|
||||
false, // mandatory
|
||||
false, // immediate
|
||||
amqp.Publishing{
|
||||
ContentType: "application/json",
|
||||
Body: body,
|
||||
|
||||
@@ -7,7 +7,10 @@ import (
|
||||
"log"
|
||||
"m2pool-payment/internal/blockchain"
|
||||
"m2pool-payment/internal/blockchain/eth"
|
||||
"m2pool-payment/internal/constant"
|
||||
"m2pool-payment/internal/crypto"
|
||||
"m2pool-payment/internal/listen"
|
||||
"m2pool-payment/internal/logger"
|
||||
message "m2pool-payment/internal/msg"
|
||||
rmq "m2pool-payment/internal/queue"
|
||||
"os"
|
||||
@@ -16,116 +19,93 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const MSG_KEY string = "9f3c7a12"
|
||||
|
||||
// 状态码常量
|
||||
const (
|
||||
STATUS_FAILED = 0 // 失败
|
||||
STATUS_SUCCESS = 1 // 成功
|
||||
STATUS_PENDING = 2 // 待确认
|
||||
STATUS_VERIFY_FAILED = 3 // 验证失败
|
||||
)
|
||||
|
||||
type ServerCtx struct {
|
||||
msgKey string
|
||||
msgKey string // 解密msg-sign的密钥,启动命令参数传入
|
||||
Config message.Config
|
||||
blockChainServer *blockchain.BlockChainServer
|
||||
rmqServer *rmq.RabbitMQServer
|
||||
messageServer *listen.ListenServer
|
||||
}
|
||||
|
||||
var s_ctx ServerCtx
|
||||
|
||||
// verifyMessage 验证消息签名
|
||||
func verifyMessage(timestamp uint64, sign string) bool {
|
||||
hash_byte := crypto.Sha256Hash(fmt.Sprintf("%x", timestamp) + MSG_KEY)
|
||||
hash := hex.EncodeToString(hash_byte)
|
||||
return hash == sign
|
||||
}
|
||||
|
||||
func loadConfig(msgKey string) {
|
||||
func loadConfig() message.Config {
|
||||
file, err := os.ReadFile("config.json")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("读取配置文件失败: %v", err))
|
||||
}
|
||||
|
||||
err = json.Unmarshal(file, &s_ctx.Config)
|
||||
var result message.Config
|
||||
err = json.Unmarshal(file, &result)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("解析配置文件失败: %v", err))
|
||||
}
|
||||
|
||||
log.Printf("✅ 配置加载成功: RPC=%s, WS=%s",
|
||||
s_ctx.Config.ETHConfig.RpcURL, s_ctx.Config.ETHConfig.WsURL)
|
||||
|
||||
s_ctx.msgKey = msgKey
|
||||
return result
|
||||
}
|
||||
|
||||
func initBlockChainServer() {
|
||||
// 初始化节点服务
|
||||
func NewServer(msgKey string) *ServerCtx {
|
||||
cfg := loadConfig()
|
||||
l := listen.NewListenServer(cfg)
|
||||
eth_node, err := eth.NewETHNode(cfg, msgKey, l)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
node_server := blockchain.NewBlockChainServer()
|
||||
// 初始化ETH节点
|
||||
eth_node, err := eth.NewETHNode(s_ctx.Config.ETHConfig, "m2pool")
|
||||
if err != nil {
|
||||
log.Fatalf("ETH-Node Start error: %v", err)
|
||||
}
|
||||
// 注册ETH节点
|
||||
node_server.RegisterChain("ETH", eth_node)
|
||||
// 将所有注册的blockChainServer绑定至server
|
||||
s_ctx.blockChainServer = node_server
|
||||
|
||||
log.Println("✅ 区块链服务初始化完成")
|
||||
}
|
||||
|
||||
func initRmqServer() {
|
||||
// 初始化rmq服务
|
||||
rmq_server, err := rmq.NewRabbitMQServer(s_ctx.Config.RMQConfig)
|
||||
rmq_server, err := rmq.NewRabbitMQServer(cfg.RmqConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("RabbitMQ Server Start error: %v", err)
|
||||
panic(err)
|
||||
}
|
||||
return &ServerCtx{
|
||||
msgKey: msgKey,
|
||||
Config: cfg,
|
||||
blockChainServer: node_server,
|
||||
rmqServer: rmq_server,
|
||||
messageServer: l,
|
||||
}
|
||||
// 将rmq服务绑定至server
|
||||
s_ctx.rmqServer = rmq_server
|
||||
|
||||
log.Printf("✅ RabbitMQ服务初始化完成: %s", s_ctx.Config.RMQConfig.SubAddr)
|
||||
}
|
||||
|
||||
func handleTopupMsg() {
|
||||
s_ctx.rmqServer.OnTopupMsg = func(msg message.TopupMsg_req) {
|
||||
msg.Address = strings.ToLower(msg.Address)
|
||||
// verifyMessage 验证消息签名
|
||||
func (s *ServerCtx) verifyMessage(timestamp uint64, sign string) bool {
|
||||
hash_byte := crypto.Sha256Hash(fmt.Sprintf("%x", timestamp) + s.msgKey)
|
||||
hash := hex.EncodeToString(hash_byte)
|
||||
return hash == sign
|
||||
}
|
||||
|
||||
func (s *ServerCtx) handleTopupMsg() {
|
||||
s.rmqServer.OnTopupMsg = func(msg message.TopupMsg_req) {
|
||||
msg.Address = strings.ToLower(msg.Address)
|
||||
// 验证签名
|
||||
if !verifyMessage(msg.Timestamp, msg.Sign) {
|
||||
err := s_ctx.rmqServer.PublishTopupResp(message.TopupMsg_resp{
|
||||
if !s.verifyMessage(msg.Timestamp, msg.Sign) {
|
||||
err := s.rmqServer.PublishTopupResp(message.TopupMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
Address: msg.Address,
|
||||
Status: STATUS_VERIFY_FAILED,
|
||||
Status: constant.STATUS_VERIFY_FAILED,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
Amount: 0,
|
||||
TxHash: "",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("❌ 发布充值失败响应失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 添加监听地址
|
||||
s_ctx.blockChainServer.AddAddress(msg.Chain, msg.Address, msg)
|
||||
// 记录 RMQ -> Listen 充值请求
|
||||
logger.LogRmqToListenTopupReq(msg.QueueId, msg.Chain, msg.Symbol, msg.Address, msg.Timestamp)
|
||||
s.messageServer.ChFromRmqServer <- msg
|
||||
}
|
||||
}
|
||||
|
||||
func handleWithdrawMsg() {
|
||||
s_ctx.rmqServer.OnWithdrawMsg = func(msg message.WithdrawMsg_req) {
|
||||
func (s *ServerCtx) handleWithdrawMsg() {
|
||||
s.rmqServer.OnWithdrawMsg = func(msg message.WithdrawMsg_req) {
|
||||
msg.FromAddress = strings.ToLower(msg.FromAddress)
|
||||
msg.ToAddress = strings.ToLower(msg.ToAddress)
|
||||
|
||||
// 验证签名
|
||||
if !verifyMessage(msg.Timestamp, msg.Sign) {
|
||||
err := s_ctx.rmqServer.PublishWithdrawResp(message.WithdrawMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
Status: STATUS_VERIFY_FAILED,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
Amount: 0,
|
||||
TxHash: "",
|
||||
if !s.verifyMessage(msg.Timestamp, msg.Sign) {
|
||||
err := s.rmqServer.PublishWithdrawResp(message.WithdrawMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
Status: constant.STATUS_VERIFY_FAILED,
|
||||
FromAddress: msg.FromAddress,
|
||||
ToAddress: msg.ToAddress,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("❌ 发布提现失败响应失败: %v", err)
|
||||
@@ -133,38 +113,23 @@ func handleWithdrawMsg() {
|
||||
return
|
||||
}
|
||||
|
||||
// 执行转账
|
||||
err := s_ctx.blockChainServer.Transfer(msg.Chain, msg.Symbol, 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: "",
|
||||
})
|
||||
}
|
||||
// 记录 RMQ -> Listen 提现请求
|
||||
logger.LogRmqToListenWithdrawReq(msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, msg.Amount, msg.Fee, msg.Timestamp)
|
||||
s.messageServer.ChFromRmqServer <- msg
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handlePayMsg() {
|
||||
s_ctx.rmqServer.OnPayMsg = func(msg message.PayMsg_req) {
|
||||
func (s *ServerCtx) handlePayMsg() {
|
||||
s.rmqServer.OnPayMsg = func(msg message.PayMsg_req) {
|
||||
msg.FromAddress = strings.ToLower(msg.FromAddress)
|
||||
msg.ToAddress = strings.ToLower(msg.ToAddress)
|
||||
|
||||
// 验证签名
|
||||
if !verifyMessage(msg.Timestamp, msg.Sign) {
|
||||
err := s_ctx.rmqServer.PublishPayResp(message.PayMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
Status: STATUS_VERIFY_FAILED,
|
||||
Amount: msg.Amount,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
OrderId: msg.OrderId,
|
||||
TxHash: "",
|
||||
if !s.verifyMessage(msg.Timestamp, msg.Sign) {
|
||||
err := s.rmqServer.PublishPayResp(message.PayMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
FromAddress: msg.FromAddress,
|
||||
Status: constant.STATUS_VERIFY_FAILED,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("❌ 发布支付失败响应失败: %v", err)
|
||||
@@ -172,86 +137,97 @@ func handlePayMsg() {
|
||||
return
|
||||
}
|
||||
|
||||
// 执行转账
|
||||
err := s_ctx.blockChainServer.Transfer(msg.Chain, msg.Symbol, msg)
|
||||
if err != nil {
|
||||
log.Printf("❌ 支付转账失败: %v", err)
|
||||
// 发送失败响应
|
||||
s_ctx.rmqServer.PublishPayResp(message.PayMsg_resp{
|
||||
// 记录 RMQ -> Listen 支付请求
|
||||
logger.LogRmqToListenPayReq(msg.QueueId, msg.Chain, msg.Symbol, msg.FromAddress, msg.ToAddress, msg.Amount, msg.Fee, msg.Timestamp)
|
||||
s.messageServer.ChFromRmqServer <- msg
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerCtx) handleRemoveMsg() {
|
||||
s.rmqServer.OnRemoveMsg = func(msg message.RemoveListenMsg_req) {
|
||||
msg.Address = strings.ToLower(msg.Address)
|
||||
// 验证签名
|
||||
if !s.verifyMessage(msg.Timestamp, msg.Sign) {
|
||||
err := s.rmqServer.PublishRemoveResp(message.RemoveListenMsg_resp{
|
||||
QueueId: msg.QueueId,
|
||||
Status: STATUS_FAILED,
|
||||
Amount: msg.Amount,
|
||||
MsgType: msg.MsgType,
|
||||
Chain: msg.Chain,
|
||||
Symbol: msg.Symbol,
|
||||
OrderId: msg.OrderId,
|
||||
TxHash: "",
|
||||
Address: msg.Address,
|
||||
Status: constant.STATUS_VERIFY_FAILED,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("❌ 发布移除监听失败响应失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 记录 RMQ -> Listen 移除监听请求
|
||||
logger.LogRmqToListenRemoveReq(msg.QueueId, msg.MsgType, msg.Chain, msg.Symbol, msg.Address, msg.Timestamp)
|
||||
s.messageServer.ChFromRmqServer <- msg
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerCtx) handleRespMsg() {
|
||||
for msg := range s.messageServer.ChToRmqServer {
|
||||
switch v := msg.(type) {
|
||||
case message.TopupMsg_resp:
|
||||
log.Printf("📨[充值响应]:QueueID=%s, Address=%s, Chain=%s, Symbol=%s, TxHash=%s, Status=%d, Amount=%f", v.QueueId, v.Address, v.Chain, v.Symbol, v.TxHash, v.Status, v.Amount)
|
||||
// 记录 Listen -> RMQ 充值响应
|
||||
logger.LogListenToRmqTopupResp(v.QueueId, v.Chain, v.Symbol, v.Address, v.FromAddress, v.TxHash, v.Amount, v.BlockHeight, v.Status)
|
||||
err := s.rmqServer.PublishTopupResp(v)
|
||||
if err != nil {
|
||||
log.Printf("❌ 发送充值响应失败: %v", err)
|
||||
return
|
||||
}
|
||||
case message.WithdrawMsg_resp:
|
||||
log.Printf("📨[提现响应]:QueueID=%s, Chain=%s, Symbol=%s, FromAddress=%s, ToAddress=%s, TxHash=%s, Status=%d, Amount=%f", v.QueueId, v.Chain, v.Symbol, v.FromAddress, v.ToAddress, v.TxHash, v.Status, v.Amount)
|
||||
// 记录 Listen -> RMQ 提现响应
|
||||
logger.LogListenToRmqWithdrawResp(v.QueueId, v.Chain, v.Symbol, v.FromAddress, v.ToAddress, v.TxHash, v.Amount, v.Fee, v.BlockHeight, v.Status)
|
||||
err := s.rmqServer.PublishWithdrawResp(v)
|
||||
if err != nil {
|
||||
log.Printf("❌ 发送提现响应失败: %v", err)
|
||||
return
|
||||
}
|
||||
case message.PayMsg_resp:
|
||||
log.Printf("📨[提现响应]:QueueID=%s, Chain=%s, Symbol=%s, FromAddress=%s, Status=%d", v.QueueId, v.Chain, v.Symbol, v.FromAddress, v.Status)
|
||||
// 记录 Listen -> RMQ 支付响应
|
||||
logger.LogListenToRmqPayResp(v.QueueId, v.Chain, v.Symbol, v.FromAddress, v.ToAddress, v.TxHash, v.Amount, v.Fee, v.BlockHeight, v.Status)
|
||||
err := s.rmqServer.PublishPayResp(v)
|
||||
if err != nil {
|
||||
log.Printf("❌ 发送支付响应失败: %v", err)
|
||||
return
|
||||
}
|
||||
case message.RemoveListenMsg_resp:
|
||||
log.Printf("📨[充值响应]:QueueID=%s, Address=%s, Chain=%s, Symbol=%s,Status=%d", v.QueueId, v.Address, v.Chain, v.Symbol, v.Status)
|
||||
// 记录 Listen -> RMQ 移除监听响应
|
||||
logger.LogListenToRmqRemoveResp(v.QueueId, v.MsgType, v.Chain, v.Symbol, v.Address, v.Status)
|
||||
err := s.rmqServer.PublishRemoveResp(v)
|
||||
if err != nil {
|
||||
log.Printf("❌ 发送移除监听响应失败: %v", err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Printf("❌ 错误响应结构: %v", v)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initRmqListen() {
|
||||
// ================== 设置 RabbitMQ 消息处理回调 ==================
|
||||
// 先设置所有回调(同步执行,避免竞态)
|
||||
handleTopupMsg()
|
||||
handleWithdrawMsg()
|
||||
handlePayMsg()
|
||||
|
||||
// 回调设置完成后,再启动 RabbitMQ 监听
|
||||
if err := s_ctx.rmqServer.Start(); err != nil {
|
||||
log.Fatalf("启动 RabbitMQ 监听失败: %v", err)
|
||||
func (s *ServerCtx) initDB() {
|
||||
// msgDB_path := s.Config.MsgConfig.SqlitePath
|
||||
// ethDB_path := s.Config.ETHConfig.SqlitePath
|
||||
msg_sql_script_path := "../public/msg.sql"
|
||||
msg_sql_byte, err := os.ReadFile(msg_sql_script_path)
|
||||
if err != nil {
|
||||
log.Fatalf("open msg-sql file error: %v", err)
|
||||
return
|
||||
}
|
||||
log.Println("✅ RabbitMQ 监听启动完成")
|
||||
}
|
||||
|
||||
func handleChainEvent(chainEventCh chan any) {
|
||||
for event := range chainEventCh {
|
||||
// 添加 panic 恢复,防止单个消息处理错误导致整个 goroutine 退出
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("❌ 处理链上事件 panic: %v, event: %+v", r, event)
|
||||
}
|
||||
}()
|
||||
|
||||
// 根据消息类型发送不同的响应
|
||||
switch msg := event.(type) {
|
||||
case message.TopupMsg_resp:
|
||||
// 充值确认
|
||||
if msg.Status == STATUS_PENDING {
|
||||
log.Printf("📨 [链上] 充值待确认: Address=%s, Amount=%.2f, TxHash=%s",
|
||||
msg.Address, msg.Amount, msg.TxHash)
|
||||
} else {
|
||||
log.Printf("✅ [链上] 充值确认: Address=%s, Amount=%.2f, TxHash=%s, Status=%d",
|
||||
msg.Address, msg.Amount, msg.TxHash, msg.Status)
|
||||
}
|
||||
err := s_ctx.rmqServer.PublishTopupResp(msg)
|
||||
if err != nil {
|
||||
log.Printf("❌ 发送充值响应失败: %v", err)
|
||||
}
|
||||
|
||||
case message.WithdrawMsg_resp:
|
||||
// 提现确认
|
||||
log.Printf("✅ [链上] 提现确认: QueueId=%s, Amount=%.2f, TxHash=%s, Status=%d",
|
||||
msg.QueueId, msg.Amount, msg.TxHash, msg.Status)
|
||||
err := s_ctx.rmqServer.PublishWithdrawResp(msg)
|
||||
if err != nil {
|
||||
log.Printf("❌ 发送提现响应失败: %v", err)
|
||||
}
|
||||
|
||||
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)
|
||||
err := s_ctx.rmqServer.PublishPayResp(msg)
|
||||
if err != nil {
|
||||
log.Printf("❌ 发送支付响应失败: %v", err)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Printf("⚠️ 未知消息类型: %T", event)
|
||||
}
|
||||
}()
|
||||
err = s.messageServer.SqliteDB.CreateTable(string(msg_sql_byte))
|
||||
if err != nil {
|
||||
log.Fatalf("exec msg-sql error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,24 +236,37 @@ func Start(msgKey string) {
|
||||
log.Println("🚀 M2Pool Payment System Starting...")
|
||||
log.Println("========================================")
|
||||
|
||||
// 加载配置
|
||||
loadConfig(msgKey)
|
||||
server := NewServer(msgKey)
|
||||
server.initDB()
|
||||
// 启动消息处理
|
||||
go server.handleTopupMsg()
|
||||
go server.handleWithdrawMsg()
|
||||
go server.handlePayMsg()
|
||||
go server.handleRemoveMsg()
|
||||
go server.handleRespMsg()
|
||||
|
||||
// ================== 初始化区块链节点 ==================
|
||||
initBlockChainServer()
|
||||
// 启动 RabbitMQ 服务
|
||||
if err := server.rmqServer.Start(); err != nil {
|
||||
log.Fatalf("❌ 启动 RabbitMQ 服务失败: %v", err)
|
||||
}
|
||||
log.Println("✅ RabbitMQ 服务启动成功")
|
||||
|
||||
// ================== 初始化 RabbitMQ 服务 ==================
|
||||
initRmqServer()
|
||||
// 启动消息服务器监听
|
||||
go server.messageServer.RmqMsgIn()
|
||||
go server.messageServer.NetMsgIn()
|
||||
log.Println("✅ 消息服务器监听启动成功")
|
||||
|
||||
// ================== 启动链上事件监听通道 ==================
|
||||
chainEventCh := make(chan any, 1000) // 增加缓冲区,避免高并发丢消息
|
||||
go s_ctx.blockChainServer.Listen("ETH", "USDT", chainEventCh)
|
||||
// 启动区块链监听
|
||||
if err := server.blockChainServer.Listen("ETH", nil); err != nil {
|
||||
log.Fatalf("❌ 启动区块链监听失败: %v", err)
|
||||
}
|
||||
log.Println("✅ 区块链监听启动成功")
|
||||
|
||||
// ================== 启动 RabbitMQ 监听 ==================
|
||||
initRmqListen()
|
||||
|
||||
// ================== 处理链上确认事件 ==================
|
||||
go handleChainEvent(chainEventCh)
|
||||
// 启动区块链消息监听
|
||||
if err := server.blockChainServer.ListenMsg("ETH"); err != nil {
|
||||
log.Fatalf("❌ 启动区块链消息监听失败: %v", err)
|
||||
}
|
||||
log.Println("✅ 区块链消息监听启动成功")
|
||||
|
||||
log.Println("========================================")
|
||||
log.Println("🎉 所有服务启动完成!")
|
||||
@@ -292,8 +281,9 @@ func Start(msgKey string) {
|
||||
log.Println("🛑 收到退出信号,正在关闭服务...")
|
||||
log.Println("========================================")
|
||||
|
||||
s_ctx.blockChainServer.Stop("ETH")
|
||||
s_ctx.rmqServer.Close()
|
||||
server.blockChainServer.Stop("ETH")
|
||||
server.rmqServer.Close()
|
||||
logger.CloseTransactionLogger()
|
||||
|
||||
log.Println("👋 服务已全部关闭")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,14 @@ import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func BigIntETHToFloat64(value *big.Int) float64 {
|
||||
f := new(big.Float).SetInt(value)
|
||||
scale := new(big.Float).SetFloat64(1e18) // USDT 精度 6 位
|
||||
f.Quo(f, scale)
|
||||
result, _ := f.Float64()
|
||||
return result
|
||||
}
|
||||
|
||||
func BigIntUSDTToFloat64(value *big.Int) float64 {
|
||||
f := new(big.Float).SetInt(value)
|
||||
scale := new(big.Float).SetFloat64(1e6) // USDT 精度 6 位
|
||||
@@ -26,6 +34,16 @@ func Float64ToBigIntUSDT(amount float64) *big.Int {
|
||||
return bigAmount
|
||||
}
|
||||
|
||||
const ETHDecimals = 18
|
||||
|
||||
func Float64ToBigIntETH(amount float64) *big.Int {
|
||||
// 乘上精度系数
|
||||
scale := math.Pow10(ETHDecimals)
|
||||
bigAmount := new(big.Int)
|
||||
bigAmount.SetInt64(int64(amount * scale))
|
||||
return bigAmount
|
||||
}
|
||||
|
||||
func Slice_delete(arr []any, index int) []any {
|
||||
if index < 0 || index >= len(arr) {
|
||||
// 处理越界
|
||||
@@ -38,3 +56,61 @@ func Slice_delete(arr []any, index int) []any {
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// 函数:检查一个切片是否包含某个字符串
|
||||
func Contains(slice []string, str string) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Float64ToBigInt(symbol string, amount float64) *big.Int {
|
||||
var scale float64
|
||||
switch symbol {
|
||||
case "ETH":
|
||||
scale = math.Pow10(ETHDecimals)
|
||||
case "USDT":
|
||||
scale = math.Pow10(USDTDecimals)
|
||||
default:
|
||||
log.Printf("don`t support symbol: %s", symbol)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查 amount 是否为负数
|
||||
if amount < 0 {
|
||||
log.Printf("Warning: negative amount for symbol %s: %f", symbol, amount)
|
||||
amount = 0 // 可以选择将负数设置为 0 或返回错误
|
||||
}
|
||||
|
||||
// 将 amount 转换为 big.Float 来避免溢出
|
||||
bigAmount := new(big.Float)
|
||||
bigAmount.SetFloat64(amount)
|
||||
|
||||
// 乘以 scale(小数位数),避免精度丢失
|
||||
bigAmount = bigAmount.Mul(bigAmount, big.NewFloat(scale))
|
||||
|
||||
// 将 big.Float 转换为 big.Int(取整)
|
||||
intAmount, _ := bigAmount.Int(nil)
|
||||
|
||||
return intAmount
|
||||
}
|
||||
|
||||
func BigIntToFloat64(symbol string, amount *big.Int) float64 {
|
||||
var scale *big.Float
|
||||
f := new(big.Float).SetInt(amount)
|
||||
switch symbol {
|
||||
case "ETH":
|
||||
scale = new(big.Float).SetFloat64(1e18)
|
||||
case "USDT":
|
||||
scale = new(big.Float).SetFloat64(1e6)
|
||||
default:
|
||||
log.Printf("don`t support symbol: %s", symbol)
|
||||
return 0
|
||||
}
|
||||
f.Quo(f, scale)
|
||||
result, _ := f.Float64()
|
||||
return result
|
||||
}
|
||||
|
||||
45
public/eth.sql
Normal file
45
public/eth.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
CREATE TABLE IF NOT EXISTS ETH_wallets (
|
||||
address TEXT PRIMARY KEY NOT NULL,
|
||||
queue_id TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
sign TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0 -- 0未在监听 1正在监听
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ETH_balances (
|
||||
address TEXT NOT NULL,
|
||||
symbol TEXT DEFAULT "ETH",
|
||||
used_gas TEXT DEFAULT "0",
|
||||
balance TEXT DEFAULT "0",
|
||||
success_tx_hash TEXT DEFAULT NULL, --使用,隔开
|
||||
failed_tx_hash TEXT DEFAULT NULL, --使用,隔开
|
||||
PRIMARY KEY (address), -- 钱包地址唯一
|
||||
FOREIGN KEY (address) REFERENCES eth_wallets (address) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS USDT_balances (
|
||||
address TEXT NOT NULL,
|
||||
symbol TEXT DEFAULT "USDT",
|
||||
freeze_num TEXT DEFAULT "0",
|
||||
balance TEXT DEFAULT "0",
|
||||
success_tx_hash TEXT DEFAULT NULL, --使用,隔开
|
||||
failed_tx_hash TEXT DEFAULT NULL, --使用,隔开
|
||||
PRIMARY KEY (address), -- 钱包地址唯一
|
||||
FOREIGN KEY (address) REFERENCES eth_wallets (address) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS eth_unconfirmed_tx (
|
||||
queue_id TEXT NOT NULL, -- 关联的msg queue_id
|
||||
tx_type INTEGER NOT NULL, --0充值,1提现,2支付
|
||||
chain TEXT DEFALUT "ETH",
|
||||
symbol TEXT,
|
||||
from_addr TEXT,
|
||||
to_addr TEXT,
|
||||
tx_hash TEXT,
|
||||
height INTEGER,
|
||||
amount TEXT,
|
||||
status INTEGER DEFAULT 2, --0充值失败,1充值成功,2充值待确认
|
||||
PRIMARY KEY (tx_hash),
|
||||
FOREIGN KEY (queue_id) REFERENCES eth_wallets (queue_id) ON DELETE CASCADE
|
||||
);
|
||||
45
public/eth_mysql.sql
Normal file
45
public/eth_mysql.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
CREATE TABLE IF NOT EXISTS ETH_wallets (
|
||||
address VARCHAR(255) PRIMARY KEY NOT NULL,
|
||||
queue_id VARCHAR(255) NOT NULL,
|
||||
timestamp BIGINT NOT NULL,
|
||||
sign VARCHAR(255) NOT NULL,
|
||||
status TINYINT DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX idx_queue_id ON ETH_wallets (queue_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ETH_balances (
|
||||
address VARCHAR(255) NOT NULL,
|
||||
symbol VARCHAR(32) DEFAULT "ETH",
|
||||
used_gas DECIMAL(40,16) DEFAULT 0,
|
||||
balance DECIMAL(40,16) DEFAULT 0,
|
||||
success_tx_hash TEXT DEFAULT NULL,
|
||||
failed_tx_hash TEXT DEFAULT NULL,
|
||||
PRIMARY KEY (address),
|
||||
FOREIGN KEY (address) REFERENCES ETH_wallets (address) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS USDT_balances (
|
||||
address VARCHAR(255) NOT NULL,
|
||||
symbol VARCHAR(32) DEFAULT "USDT",
|
||||
freeze_num DECIMAL(40,16) DEFAULT 0,
|
||||
balance DECIMAL(40,16) DEFAULT 0,
|
||||
success_tx_hash TEXT DEFAULT NULL,
|
||||
failed_tx_hash TEXT DEFAULT NULL,
|
||||
PRIMARY KEY (address),
|
||||
FOREIGN KEY (address) REFERENCES ETH_wallets (address) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ETH_unconfirmed_tx (
|
||||
queue_id VARCHAR(255) NOT NULL,
|
||||
tx_type TINYINT NOT NULL,
|
||||
chain VARCHAR(32) DEFAULT "ETH",
|
||||
symbol VARCHAR(32),
|
||||
from_addr VARCHAR(255),
|
||||
to_addr VARCHAR(255),
|
||||
tx_hash VARCHAR(255),
|
||||
height BIGINT,
|
||||
amount DECIMAL(40,16),
|
||||
status TINYINT DEFAULT 2,
|
||||
FOREIGN KEY (queue_id) REFERENCES ETH_wallets (queue_id) ON DELETE CASCADE
|
||||
);
|
||||
3
public/infura
Normal file
3
public/infura
Normal file
@@ -0,0 +1,3 @@
|
||||
API-KEY=431dbd08d89c406fb9f2b5ee812f5faf
|
||||
HTTPS=https://mainnet.infura.io/v3/431dbd08d89c406fb9f2b5ee812f5faf
|
||||
mnemonic=income miracle spin apple live cigar trim social age push eye chase
|
||||
113
public/msg.sql
Normal file
113
public/msg.sql
Normal file
@@ -0,0 +1,113 @@
|
||||
CREATE TABLE IF NOT EXISTS topup_req_msg (
|
||||
queue_id TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
timestamp INTEGER,
|
||||
sign TEXT,
|
||||
status INTEGER DEFAULT 0, -- 0未在监听,1正在监听
|
||||
PRIMARY KEY (queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS topup_resp_msg (
|
||||
queue_id TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
from_addr TEXT DEFAULT NULL,
|
||||
to_addr TEXT NOT NULL,
|
||||
amount TEXT NOT NULL, -- 改为 TEXT 类型
|
||||
tx_hash TEXT DEFAULT NULL,
|
||||
height INTEGER DEFAULT NULL,
|
||||
status INTEGER DEFAULT 5,
|
||||
FOREIGN KEY (queue_id) REFERENCES topup_req_msg(queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS withdraw_req_msg (
|
||||
queue_id TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
from_addr TEXT NOT NULL,
|
||||
to_addr TEXT NOT NULL,
|
||||
amount TEXT NOT NULL, -- 改为 TEXT 类型
|
||||
fee TEXT NOT NULL, -- 改为 TEXT 类型
|
||||
timestamp INTEGER,
|
||||
sign TEXT,
|
||||
status INTEGER DEFAULT 5,
|
||||
PRIMARY KEY (queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS withdraw_resp_msg (
|
||||
queue_id TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
from_addr TEXT NOT NULL,
|
||||
to_addr TEXT NOT NULL,
|
||||
tx_hash TEXT DEFAULT NULL,
|
||||
amount TEXT DEFAULT NULL, -- 改为 TEXT 类型
|
||||
fee TEXT DEFAULT NULL, -- 改为 TEXT 类型
|
||||
height INTEGER DEFAULT NULL,
|
||||
status INTEGER DEFAULT 5,
|
||||
FOREIGN KEY (queue_id) REFERENCES withdraw_req_msg(queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pay_req_msg (
|
||||
queue_id TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
from_addr TEXT NOT NULL,
|
||||
to_addr TEXT NOT NULL,
|
||||
amount TEXT NOT NULL, -- 改为 TEXT 类型
|
||||
fee TEXT NOT NULL, -- 改为 TEXT 类型
|
||||
timestamp INTEGER,
|
||||
sign TEXT,
|
||||
status INTEGER DEFAULT 5,
|
||||
PRIMARY KEY (queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pay_resp_msg (
|
||||
queue_id TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
from_addr TEXT NOT NULL,
|
||||
to_addr TEXT NOT NULL,
|
||||
tx_hash TEXT DEFAULT NULL,
|
||||
amount TEXT DEFAULT NULL, -- 改为 TEXT 类型
|
||||
fee TEXT DEFAULT NULL, -- 改为 TEXT 类型
|
||||
height INTEGER DEFAULT NULL,
|
||||
status INTEGER DEFAULT 5,
|
||||
FOREIGN KEY (queue_id) REFERENCES pay_req_msg(queue_id)
|
||||
);
|
||||
|
||||
-- pay_msg的交易
|
||||
-- CREATE TABLE IF NOT EXISTS pay_msg_tx (
|
||||
-- queue_id TEXT NOT NULL,
|
||||
-- tx_hash TEXT DEFAULT NULL,
|
||||
-- to_addr TEXT NOT NULL,
|
||||
-- amount TEXT DEFAULT NULL, -- 改为 TEXT 类型
|
||||
-- fee TEXT, -- 改为 TEXT 类型
|
||||
-- height INTEGER DEFAULT 0,
|
||||
-- status INTEGER DEFAULT 5,
|
||||
-- FOREIGN KEY (queue_id) REFERENCES pay_req_msg(queue_id)
|
||||
-- );
|
||||
|
||||
CREATE TABLE IF NOT EXISTS remove_req_msg(
|
||||
queue_id TEXT NOT NULL,
|
||||
msg_type INTEGER NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
timestamp INTEGER,
|
||||
sign TEXT,
|
||||
status INTEGER DEFAULT 2,
|
||||
PRIMARY KEY (queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS remove_resp_msg(
|
||||
queue_id TEXT NOT NULL,
|
||||
msg_type INTEGER NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 2,
|
||||
FOREIGN KEY (queue_id) REFERENCES remove_req_msg(queue_id)
|
||||
);
|
||||
101
public/msg_mysql.sql
Normal file
101
public/msg_mysql.sql
Normal file
@@ -0,0 +1,101 @@
|
||||
CREATE TABLE IF NOT EXISTS topup_req_msg (
|
||||
queue_id VARCHAR(255) NOT NULL,
|
||||
chain VARCHAR(32) NOT NULL,
|
||||
symbol VARCHAR(32) NOT NULL,
|
||||
address VARCHAR(255) NOT NULL,
|
||||
timestamp BIGINT,
|
||||
sign VARCHAR(255),
|
||||
status TINYINT DEFAULT 1,
|
||||
PRIMARY KEY (queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS topup_resp_msg (
|
||||
queue_id VARCHAR(255) NOT NULL,
|
||||
chain VARCHAR(32) NOT NULL,
|
||||
symbol VARCHAR(32) NOT NULL,
|
||||
from_addr VARCHAR(255) NOT NULL,
|
||||
to_addr VARCHAR(255) NOT NULL,
|
||||
amount DECIMAL(30,16) NOT NULL,
|
||||
tx_hash VARCHAR(255) DEFAULT NULL,
|
||||
height BIGINT DEFAULT NULL,
|
||||
status TINYINT DEFAULT 5,
|
||||
FOREIGN KEY (queue_id) REFERENCES topup_req_msg(queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS withdraw_req_msg (
|
||||
queue_id VARCHAR(255) NOT NULL,
|
||||
chain VARCHAR(32) NOT NULL,
|
||||
symbol VARCHAR(32) NOT NULL,
|
||||
from_addr VARCHAR(255) NOT NULL,
|
||||
to_addr VARCHAR(255) NOT NULL,
|
||||
amount DECIMAL(30,16) NOT NULL,
|
||||
fee DECIMAL(30,16) NOT NULL,
|
||||
timestamp BIGINT,
|
||||
sign VARCHAR(255),
|
||||
status TINYINT DEFAULT 5,
|
||||
PRIMARY KEY (queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS withdraw_resp_msg (
|
||||
queue_id VARCHAR(255) NOT NULL,
|
||||
chain VARCHAR(32) NOT NULL,
|
||||
symbol VARCHAR(32) NOT NULL,
|
||||
from_addr VARCHAR(255) NOT NULL,
|
||||
to_addr VARCHAR(255) NOT NULL,
|
||||
tx_hash VARCHAR(255) DEFAULT NULL,
|
||||
amount DECIMAL(30,16) DEFAULT NULL,
|
||||
fee DECIMAL(30,16) DEFAULT NULL,
|
||||
height BIGINT DEFAULT NULL,
|
||||
status TINYINT DEFAULT 5,
|
||||
FOREIGN KEY (queue_id) REFERENCES withdraw_req_msg(queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pay_req_msg (
|
||||
queue_id VARCHAR(255) NOT NULL,
|
||||
chain VARCHAR(32) NOT NULL,
|
||||
symbol VARCHAR(32) NOT NULL,
|
||||
from_addr VARCHAR(255) NOT NULL,
|
||||
to_addr VARCHAR(255) NOT NULL,
|
||||
amount DECIMAL(30,16) NOT NULL,
|
||||
fee DECIMAL(30,16) NOT NULL,
|
||||
timestamp BIGINT,
|
||||
sign VARCHAR(255),
|
||||
status TINYINT DEFAULT 5,
|
||||
PRIMARY KEY (queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pay_resp_msg (
|
||||
queue_id VARCHAR(255) NOT NULL,
|
||||
chain VARCHAR(32) NOT NULL,
|
||||
symbol VARCHAR(32) NOT NULL,
|
||||
from_addr VARCHAR(255) NOT NULL,
|
||||
to_addr VARCHAR(255) NOT NULL,
|
||||
tx_hash VARCHAR(255) DEFAULT NULL,
|
||||
amount DECIMAL(30,16) DEFAULT NULL,
|
||||
fee DECIMAL(30,16) DEFAULT NULL,
|
||||
height BIGINT DEFAULT NULL,
|
||||
status TINYINT DEFAULT 5,
|
||||
FOREIGN KEY (queue_id) REFERENCES pay_req_msg(queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS remove_req_msg(
|
||||
queue_id VARCHAR(255) NOT NULL,
|
||||
msg_type TINYINT NOT NULL,
|
||||
chain VARCHAR(32) NOT NULL,
|
||||
symbol VARCHAR(32) NOT NULL,
|
||||
address VARCHAR(255) NOT NULL,
|
||||
timestamp BIGINT,
|
||||
sign VARCHAR(255),
|
||||
status TINYINT DEFAULT 2,
|
||||
PRIMARY KEY (queue_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS remove_resp_msg(
|
||||
queue_id VARCHAR(255) NOT NULL,
|
||||
msg_type TINYINT NOT NULL,
|
||||
chain VARCHAR(32) NOT NULL,
|
||||
symbol VARCHAR(32) NOT NULL,
|
||||
address VARCHAR(255) NOT NULL,
|
||||
status TINYINT DEFAULT 2,
|
||||
FOREIGN KEY (queue_id) REFERENCES remove_req_msg(queue_id)
|
||||
);
|
||||
22
流程.txt
Normal file
22
流程.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
充值:rmq -> listen -> 写数据库
|
||||
-> 添加到TopupMsgs
|
||||
-> 传给对应的node server -> node server接收 -> 添加到钱包
|
||||
-> 记录到钱包数据库
|
||||
|
||||
提现/支付:rmq -> listen -> 写数据库
|
||||
-> 添加到WithdrawMsgs
|
||||
-> 传给对应的node server -> node server接收 -> 记录到数据库
|
||||
-> 开始转账 -> 校验余额 -> 转账 -> 转账结果返回 -> listen -> 修改相关状态
|
||||
|
||||
node server listen -> 新区块产生时读取当前listen server的消息 -> 对比消息和区块中的交易 -> 消息中的to = 区块交易中的to(充值) -> 返回消息 -> listen -> 修改数据库
|
||||
-> 记录到unconfirmtxs -> 返回rmq -> 发送消息
|
||||
-> 消息中的from、to、amount = 区块交易中的from、to、amount(提现/充值) -> 返回消息 -> listen -> 修改数据库状态
|
||||
|
||||
node server confirm -> 新区块产生同时会读取当前unconfirmtxs数据 -> 对比每个交易的高度 -> 符合确认条件 -> 修改钱包数据
|
||||
-> 返回消息 -> listen -> 修改相关数据 -> 返回rmq -> 发出消息
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
确认后的状态修改(余额增减、hash录入等)
|
||||
Reference in New Issue
Block a user