完善商福通支付接口,修复证书加载问题;数据库扩展支付平台字段并更新支付信息保存逻辑;日志中间件异步记录日志
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
|
||||
### 长期
|
||||
|
||||
异步日志记录,统一收集
|
||||
|
||||
模型字段修改,特定枚举字段使用自定义类型代替通用 int32
|
||||
|
||||
更新接口可以传输更结构化的数据,直接区分不同类型以加快更新速度
|
||||
|
||||
4
go.mod
4
go.mod
@@ -8,7 +8,6 @@ require (
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-redsync/redsync v1.4.2
|
||||
github.com/go-redsync/redsync/v4 v4.13.0
|
||||
github.com/gofiber/fiber/v2 v2.52.6
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -43,7 +42,6 @@ require (
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-redis/redis v6.15.9+incompatible // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.1 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
@@ -65,8 +63,6 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.37.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/smartwalle/ncrypto v1.0.4 // indirect
|
||||
|
||||
37
go.sum
37
go.sum
@@ -77,9 +77,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -96,14 +93,11 @@ github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOr
|
||||
github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-redsync/redsync v1.4.2 h1:KADEZ2rlaHMZWnlkthQCxfGP+8ZWwJLiSjOYN3mntKA=
|
||||
github.com/go-redsync/redsync v1.4.2/go.mod h1:my8/M5YL986u2jBMtZTLkBIgBsKNNSixJWzWwISH6Uw=
|
||||
github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA=
|
||||
github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
|
||||
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
@@ -140,12 +134,10 @@ github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORR
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
||||
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -200,17 +192,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@@ -267,7 +248,6 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
@@ -292,7 +272,6 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@@ -302,16 +281,13 @@ golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
@@ -332,7 +308,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
@@ -341,17 +316,12 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -401,7 +371,6 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
@@ -411,7 +380,6 @@ golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
@@ -430,16 +398,11 @@ google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwl
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
8
pkg/env/env.go
vendored
8
pkg/env/env.go
vendored
@@ -352,6 +352,7 @@ func loadAliyun() {
|
||||
var (
|
||||
SftPayEnable = false
|
||||
SftPayAppId string
|
||||
SftPayRouteId string
|
||||
SftPayAppPrivateKey string
|
||||
SftPayPublicKey string
|
||||
)
|
||||
@@ -375,6 +376,11 @@ func loadSftPay() {
|
||||
SftPayAppId = value
|
||||
}
|
||||
|
||||
value = os.Getenv("SFTPAY_ROUTE_ID")
|
||||
if value != "" {
|
||||
SftPayRouteId = value
|
||||
}
|
||||
|
||||
value = os.Getenv("SFTPAY_APP_PRIVATE_KEY")
|
||||
if value == "" {
|
||||
panic("环境变量 SFTPAY_APP_PRIVATE_KEY 的值不能为空")
|
||||
@@ -442,5 +448,5 @@ func Init() {
|
||||
loadAlipay()
|
||||
loadWechatPay()
|
||||
loadAliyun()
|
||||
// loadSftPay()
|
||||
loadSftPay()
|
||||
}
|
||||
|
||||
@@ -805,7 +805,8 @@ create table trade (
|
||||
amount decimal(12, 2) not null default 0,
|
||||
payment decimal(12, 2) not null default 0,
|
||||
method int not null,
|
||||
acquirer int not null,
|
||||
platform int not null,
|
||||
acquirer int ,
|
||||
status int not null default 0,
|
||||
pay_url text,
|
||||
paid_at timestamp,
|
||||
@@ -832,6 +833,7 @@ comment on column trade.remark is '订单备注';
|
||||
comment on column trade.amount is '订单总金额';
|
||||
comment on column trade.payment is '支付金额';
|
||||
comment on column trade.method is '支付方式:1-支付宝,2-微信,3-商福通渠道支付宝,4-商福通渠道微信';
|
||||
comment on column trade.platform is '支付平台:1-电脑网站,2-手机网站';
|
||||
comment on column trade.acquirer is '收单机构:1-支付宝,2-微信,3-银联';
|
||||
comment on column trade.status is '订单状态:0-待支付,1-已支付,2-已取消,3-已退款';
|
||||
comment on column trade.pay_url is '支付链接';
|
||||
|
||||
@@ -49,7 +49,7 @@ type PageResp struct {
|
||||
|
||||
// region error
|
||||
|
||||
type BizErr struct {
|
||||
type Err struct {
|
||||
msg string
|
||||
err error
|
||||
|
||||
@@ -58,18 +58,18 @@ type BizErr struct {
|
||||
errFunc string
|
||||
}
|
||||
|
||||
func (e *BizErr) Error() string {
|
||||
func (e *Err) Error() string {
|
||||
if e.err != nil {
|
||||
return e.msg + ":" + e.err.Error()
|
||||
}
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *BizErr) Unwrap() error {
|
||||
func (e *Err) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e *BizErr) Source() *slog.Source {
|
||||
func (e *Err) Source() *slog.Source {
|
||||
return &slog.Source{
|
||||
Function: e.errFunc,
|
||||
File: e.errFile,
|
||||
@@ -77,25 +77,37 @@ func (e *BizErr) Source() *slog.Source {
|
||||
}
|
||||
}
|
||||
|
||||
func NewBizErr(msg string, err ...error) (biz *BizErr) {
|
||||
biz = &BizErr{
|
||||
func newErr(msg string, err ...error) Err {
|
||||
o := Err{
|
||||
msg: msg,
|
||||
}
|
||||
|
||||
if len(err) > 0 {
|
||||
biz.err = err[0]
|
||||
o.err = err[0]
|
||||
}
|
||||
|
||||
if env.RunMode == env.RunModeDev {
|
||||
pc, file, line, ok := runtime.Caller(1)
|
||||
pc, file, line, ok := runtime.Caller(2)
|
||||
if ok {
|
||||
biz.errFile = file
|
||||
biz.errLine = line
|
||||
biz.errFunc = runtime.FuncForPC(pc).Name()
|
||||
o.errFile = file
|
||||
o.errLine = line
|
||||
o.errFunc = runtime.FuncForPC(pc).Name()
|
||||
}
|
||||
}
|
||||
|
||||
return biz
|
||||
return o
|
||||
}
|
||||
|
||||
type BizErr struct{ Err }
|
||||
|
||||
func NewBizErr(msg string, err ...error) (biz *BizErr) {
|
||||
return &BizErr{newErr(msg, err...)}
|
||||
}
|
||||
|
||||
type ServErr struct{ Err }
|
||||
|
||||
func NewServErr(msg string, err ...error) *ServErr {
|
||||
return &ServErr{newErr(msg, err...)}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -17,6 +17,13 @@ const (
|
||||
MethodSftWeChat // 商福通渠道指定微信
|
||||
)
|
||||
|
||||
type Platform int32
|
||||
|
||||
const (
|
||||
PlatformDesktop Platform = iota + 1 // 桌面网站
|
||||
PlatformMobile // 手机网站
|
||||
)
|
||||
|
||||
type Acquirer int32
|
||||
|
||||
const (
|
||||
|
||||
@@ -46,7 +46,7 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
|
||||
|
||||
// 所有未手动声明的错误类型
|
||||
default:
|
||||
slog.Debug("未处理的异常", slog.String("type", reflect.TypeOf(err).Name()), slog.String("error", err.Error()))
|
||||
slog.Warn("未处理的异常", slog.String("type", reflect.TypeOf(err).Name()), slog.String("error", err.Error()))
|
||||
}
|
||||
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
var Asynq *asynq.Client
|
||||
|
||||
func InitAsynq() {
|
||||
func initAsynq() {
|
||||
var client = asynq.NewClientFromRedisClient(Redis)
|
||||
Asynq = client
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ func Init() {
|
||||
initRedis()
|
||||
initOrm()
|
||||
initProxy()
|
||||
InitAsynq()
|
||||
initAsynq()
|
||||
initSft()
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"platform/web/core"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -21,29 +23,31 @@ var SFTPay SftClient
|
||||
|
||||
type SftClient struct {
|
||||
appid string
|
||||
routeId string
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey *rsa.PublicKey
|
||||
}
|
||||
|
||||
func init() {
|
||||
func initSft() {
|
||||
if !env.SftPayEnable {
|
||||
return
|
||||
panic("商福通支付未启用,请检查环境变量 SFTPAY_ENABLE")
|
||||
}
|
||||
|
||||
SFTPay = SftClient{
|
||||
appid: env.SftPayAppId,
|
||||
appid: env.SftPayAppId,
|
||||
routeId: env.SftPayRouteId,
|
||||
}
|
||||
|
||||
// 加载私钥
|
||||
block, _ := pem.Decode([]byte(env.SftPayAppPrivateKey))
|
||||
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
||||
panic("加载商福通私钥失败")
|
||||
private, err := base64.StdEncoding.DecodeString(env.SftPayAppPrivateKey)
|
||||
if err != nil {
|
||||
panic("解析商福通私钥失败: " + err.Error())
|
||||
}
|
||||
|
||||
var privateKey *rsa.PrivateKey
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
privateKey, err = x509.ParsePKCS1PrivateKey(private)
|
||||
if err != nil {
|
||||
pkcs8, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
pkcs8, err := x509.ParsePKCS8PrivateKey(private)
|
||||
if err != nil {
|
||||
panic("解析商福通私钥失败: " + err.Error())
|
||||
}
|
||||
@@ -57,13 +61,13 @@ func init() {
|
||||
SFTPay.privateKey = privateKey
|
||||
|
||||
// 加载公钥
|
||||
block, _ = pem.Decode([]byte(env.SftPayPublicKey))
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
panic("加载商福通公钥失败")
|
||||
public, err := base64.StdEncoding.DecodeString(env.SftPayPublicKey)
|
||||
if err != nil {
|
||||
panic("解析商福通公钥失败: " + err.Error())
|
||||
}
|
||||
|
||||
var publicKey *rsa.PublicKey
|
||||
pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
pkix, err := x509.ParsePKIXPublicKey(public)
|
||||
if err != nil {
|
||||
panic("解析商福通公钥失败: " + err.Error())
|
||||
}
|
||||
@@ -78,11 +82,13 @@ func init() {
|
||||
|
||||
func (s *SftClient) PaymentScanPay(req *PaymentScanPayReq) (*PaymentScanPayResp, error) {
|
||||
const url = "https://pay.rscygroup.com/api/open/payment/scanpay"
|
||||
req.RouteNo = u.P(s.routeId)
|
||||
return call[PaymentScanPayResp](s, url, req)
|
||||
}
|
||||
|
||||
func (s *SftClient) PaymentH5Pay(req *PaymentH5PayReq) (*PaymentH5PayResp, error) {
|
||||
const url = "https://pay.rscygroup.com/api/open/payment/h5pay"
|
||||
req.RouteNo = u.P(s.routeId)
|
||||
return call[PaymentH5PayResp](s, url, req)
|
||||
}
|
||||
|
||||
@@ -91,6 +97,11 @@ func (s *SftClient) OrderClose(req *OrderCloseReq) (*OrderCloseResp, error) {
|
||||
return call[OrderCloseResp](s, url, req)
|
||||
}
|
||||
|
||||
func (s *SftClient) QueryTrade(req *QueryTradeReq) (*QueryTradeResp, error) {
|
||||
const url = "https://pay.rscygroup.com/api/open/query/trade"
|
||||
return call[QueryTradeResp](s, url, req)
|
||||
}
|
||||
|
||||
type PaymentScanPayReq struct {
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
@@ -132,6 +143,11 @@ type PaymentH5PayReq struct {
|
||||
LimitPay *int `json:"limitPay"`
|
||||
}
|
||||
|
||||
type QueryTradeReq struct {
|
||||
PayOrderId *string `json:"payOrderId"`
|
||||
MchOrderNo *string `json:"mchOrderNo"`
|
||||
}
|
||||
|
||||
type OrderCloseReq struct {
|
||||
MchOrderNo *string `json:"mchOrderNo"`
|
||||
PayOrderId *string `json:"payOrderId"`
|
||||
@@ -197,6 +213,29 @@ type PaymentH5PayResp struct {
|
||||
SettlementType *string `json:"settlementType"`
|
||||
}
|
||||
|
||||
type QueryTradeResp struct {
|
||||
Amount int64 `json:"amount"`
|
||||
ChannelSendNo *string `json:"channelSendNo"`
|
||||
IfCode *string `json:"ifCode"`
|
||||
MercNo string `json:"mercNo"`
|
||||
MchOrderNo string `json:"mchOrderNo"`
|
||||
PayOrderId *string `json:"payOrderId"`
|
||||
PayType string `json:"payType"`
|
||||
ChannelTradeNo *string `json:"channelTradeNo"`
|
||||
State SftTradeState `json:"state"`
|
||||
RefundAmt *int64 `json:"refundAmt"`
|
||||
RefundState int32 `json:"refundState"`
|
||||
DrType *string `json:"drType"`
|
||||
ExtParam *string `json:"extParam"`
|
||||
PayTime *string `json:"payTime"`
|
||||
Subject string `json:"subject"`
|
||||
TradeFee *int64 `json:"tradeFee"`
|
||||
CashFee *int64 `json:"cashFee"`
|
||||
StoreId *string `json:"storeId"`
|
||||
UserId *string `json:"userId"`
|
||||
SettlementType *string `json:"settlementType"`
|
||||
}
|
||||
|
||||
type OrderCloseResp struct {
|
||||
MchOrderNo string `json:"mchOrderNo"`
|
||||
PayOrderId string `json:"payOrderId"`
|
||||
@@ -224,19 +263,30 @@ func call[T any](s *SftClient, url string, req any) (*T, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败:%w", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
reqDump, err := httputil.DumpRequest(request, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求内容转储失败:%w", err)
|
||||
}
|
||||
println(string(reqDump) + "\n\n")
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败:%w", err)
|
||||
}
|
||||
|
||||
respDump, err := httputil.DumpResponse(response, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("响应内容转储失败:%w", err)
|
||||
}
|
||||
println(string(respDump) + "\n\n")
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("请求响应失败:%d", response.StatusCode)
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
defer func(body io.ReadCloser) {
|
||||
_ = body.Close()
|
||||
}(response.Body)
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
@@ -248,15 +298,9 @@ func call[T any](s *SftClient, url string, req any) (*T, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解密响应内容失败:%w", err)
|
||||
}
|
||||
if decode.Code != "000000" {
|
||||
return nil, fmt.Errorf("请求业务响应失败:%s", u.Z(decode.Msg))
|
||||
}
|
||||
if decode.BizData == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var resp = new(T)
|
||||
err = json.Unmarshal([]byte(*decode.BizData), resp)
|
||||
err = json.Unmarshal([]byte(decode), resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("响应正文解析失败:%w", err)
|
||||
}
|
||||
@@ -290,24 +334,34 @@ func (s *SftClient) sign(msg any) (*request, error) {
|
||||
return &body, nil
|
||||
}
|
||||
|
||||
func (s *SftClient) verify(str []byte) (*response, error) {
|
||||
func (s *SftClient) verify(str []byte) (string, error) {
|
||||
|
||||
var resp = new(response)
|
||||
err := json.Unmarshal(str, resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析响应正文失败:%w", err)
|
||||
return "", fmt.Errorf("解析响应正文失败:%w", err)
|
||||
}
|
||||
|
||||
if resp.Sign != nil || resp.SignType != nil || resp.BizData != nil {
|
||||
|
||||
hashed := sha256.Sum256([]byte(resp.String()))
|
||||
err := rsa.VerifyPKCS1v15(s.publicKey, crypto.SHA256, hashed[:], []byte(*resp.Sign))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("验签失败:%w", err)
|
||||
}
|
||||
if resp.Code != "000000" {
|
||||
return "", fmt.Errorf("请求业务响应失败:%s", u.Z(resp.Msg))
|
||||
}
|
||||
|
||||
return resp, err
|
||||
if resp.Sign == nil {
|
||||
return "", core.NewServErr("响应数据签名为空")
|
||||
}
|
||||
|
||||
ser, err := resp.String()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("格式化响应内容失败:%w", err)
|
||||
}
|
||||
|
||||
hashed := sha256.Sum256([]byte(ser))
|
||||
err = rsa.VerifyPKCS1v15(s.publicKey, crypto.SHA256, hashed[:], []byte(*resp.Sign))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("验签失败:%w", err)
|
||||
}
|
||||
|
||||
return *resp.BizData, nil
|
||||
}
|
||||
|
||||
type request struct {
|
||||
@@ -336,11 +390,17 @@ type response struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
func (r response) String() string {
|
||||
func (r response) String() (string, error) {
|
||||
if r.BizData == nil || r.Msg == nil || r.SignType == nil {
|
||||
return "", core.NewServErr(fmt.Sprintf(
|
||||
"上游数据返回有空值:BizData %v,Msg %v, SignType %v",
|
||||
r.BizData == nil, r.Msg == nil, r.SignType == nil,
|
||||
))
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"bizData=%s&code=%s&msg=%s&signType=%s×tamp=%s",
|
||||
u.Z(r.BizData), r.Code, u.Z(r.Msg), u.Z(r.SignType), r.Timestamp,
|
||||
)
|
||||
), nil
|
||||
}
|
||||
|
||||
type SftPayType string
|
||||
@@ -350,3 +410,16 @@ const (
|
||||
SftWeChat SftPayType = "WECHAT"
|
||||
SftUnionPay SftPayType = "UNIONPAY"
|
||||
)
|
||||
|
||||
type SftTradeState string
|
||||
|
||||
const (
|
||||
SftInit SftTradeState = "INIT"
|
||||
SftTradeAWAIT SftTradeState = "TRADE_WAIT"
|
||||
SftTradeSuccess SftTradeState = "TRADE_SUCCESS"
|
||||
SftTradeFail SftTradeState = "TRADE_FAIL"
|
||||
SftTradeCancel SftTradeState = "TRADE_CANCEL"
|
||||
SftTradeRefund SftTradeState = "TRADE_REFUND"
|
||||
SftRefundIng SftTradeState = "REFUND_ING"
|
||||
SftTradeClosed SftTradeState = "TRADE_CLOSED"
|
||||
)
|
||||
|
||||
@@ -293,7 +293,7 @@ func PrepareCreateResource(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 准备创建套餐
|
||||
result, err := s.Resource.PrepareResource(authCtx.Payload.Id, time.Now(), req.Method, &req.CreateResourceSerializer)
|
||||
result, err := s.Resource.PrepareResource(authCtx.Payload.Id, time.Now(), &req.CreateResourceSerializer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shopspring/decimal"
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
trade2 "platform/web/domains/trade"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
@@ -143,7 +145,9 @@ func UpdatePassword(c *fiber.Ctx) error {
|
||||
// region /recharge
|
||||
|
||||
type RechargePrepareReq struct {
|
||||
Amount string `json:"amount" validate:"required,numeric"`
|
||||
Amount string `json:"amount" validate:"required,numeric"`
|
||||
Platform trade2.Platform `json:"platform" validate:"required"`
|
||||
Method trade2.Method `json:"method" validate:"required"`
|
||||
}
|
||||
|
||||
type RechargePrepareResp struct {
|
||||
@@ -159,7 +163,6 @@ type RechargeConfirmResp struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// RechargePrepareAlipay 通过支付宝充值
|
||||
func RechargePrepareAlipay(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
|
||||
@@ -181,12 +184,13 @@ func RechargePrepareAlipay(c *fiber.Ctx) error {
|
||||
}
|
||||
var result *s.TradeCreateResult
|
||||
err = q.Q.Transaction(func(tx *q.Query) error {
|
||||
result, err = s.Trade.SendCreateTradeByQrcode(tx, authContext.Payload.Id, now, &s.TradeCreateData{
|
||||
result, err = s.Trade.CreateTrade(tx, authContext.Payload.Id, now, &s.TradeCreateData{
|
||||
Subject: "账户充值 - " + amount.StringFixed(2) + "元",
|
||||
Amount: amount,
|
||||
ExpireAt: time.Now().Add(30 * time.Minute),
|
||||
Type: trade2.TypeRecharge,
|
||||
Method: trade2.MethodAlipay,
|
||||
Platform: req.Platform,
|
||||
})
|
||||
return err
|
||||
})
|
||||
@@ -215,7 +219,7 @@ func RechargeConfirmAlipay(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 验证支付结果
|
||||
result, err := s.Trade.VerifyCreateTrade(&s.TradeVerifyData{
|
||||
result, err := s.Trade.VerifyTrade(&s.TradeVerifyData{
|
||||
TradeNo: req.TradeNo,
|
||||
Method: trade2.MethodAlipay,
|
||||
})
|
||||
@@ -253,12 +257,13 @@ func RechargePrepareWechat(c *fiber.Ctx) error {
|
||||
}
|
||||
var result *s.TradeCreateResult
|
||||
err = q.Q.Transaction(func(tx *q.Query) error {
|
||||
result, err = s.Trade.SendCreateTradeByQrcode(tx, authContext.Payload.Id, now, &s.TradeCreateData{
|
||||
result, err = s.Trade.CreateTrade(tx, authContext.Payload.Id, now, &s.TradeCreateData{
|
||||
Subject: "账户充值 - " + amount.StringFixed(2) + "元",
|
||||
Amount: amount,
|
||||
ExpireAt: now.Add(30 * time.Minute),
|
||||
Type: trade2.TypeRecharge,
|
||||
Method: trade2.MethodWeChat,
|
||||
Platform: req.Platform,
|
||||
})
|
||||
return err
|
||||
})
|
||||
@@ -289,7 +294,82 @@ func RechargeConfirmWechat(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 验证支付结果
|
||||
result, err := s.Trade.VerifyCreateTrade(&s.TradeVerifyData{
|
||||
result, err := s.Trade.VerifyTrade(&s.TradeVerifyData{
|
||||
TradeNo: req.TradeNo,
|
||||
Method: trade2.MethodWeChat,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
err = s.User.RechargeConfirm(req.TradeNo, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"status": "success"})
|
||||
}
|
||||
|
||||
func RechargePrepare(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(RechargePrepareReq)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 保存交易信息
|
||||
var now = time.Now()
|
||||
amount, err := decimal.NewFromString(req.Amount)
|
||||
if err != nil {
|
||||
return core.NewBizErr(fmt.Sprintf("金额格式错误: %s", err.Error()))
|
||||
}
|
||||
var result *s.TradeCreateResult
|
||||
err = q.Q.Transaction(func(tx *q.Query) error {
|
||||
result, err = s.Trade.CreateTrade(tx, authContext.Payload.Id, now, &s.TradeCreateData{
|
||||
Subject: "账户充值 - " + amount.StringFixed(2) + "元",
|
||||
Amount: amount,
|
||||
ExpireAt: now.Add(30 * time.Minute),
|
||||
Type: trade2.TypeRecharge,
|
||||
Method: req.Method,
|
||||
Platform: req.Platform,
|
||||
})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.JSON(RechargePrepareResp{
|
||||
TradeNo: result.TradeNo,
|
||||
PayURL: result.PayURL,
|
||||
})
|
||||
}
|
||||
|
||||
func RechargeConfirm(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(struct {
|
||||
TradeNo string `json:"trade_no" validate:"required"`
|
||||
})
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证支付结果
|
||||
result, err := s.Trade.VerifyTrade(&s.TradeVerifyData{
|
||||
TradeNo: req.TradeNo,
|
||||
Method: trade2.MethodWeChat,
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ type Trade struct {
|
||||
Remark *string `gorm:"column:remark;type:character varying(255);comment:订单备注" json:"remark"` // 订单备注
|
||||
Amount decimal.Decimal `gorm:"column:amount;type:numeric(12,2);not null;comment:订单总金额" json:"amount"` // 订单总金额
|
||||
Payment decimal.Decimal `gorm:"column:payment;type:numeric(12,2);not null;comment:支付金额" json:"payment"` // 支付金额
|
||||
Method int32 `gorm:"column:method;type:integer;not null;comment:支付方式:1-支付宝,2-微信,3-商福通" json:"method"` // 支付方式:1-支付宝,2-微信,3-商福通
|
||||
Method int32 `gorm:"column:method;type:integer;not null;comment:支付方式:1-支付宝,2-微信,3-商福通渠道支付宝,4-商福通渠道微信" json:"method"` // 支付方式:1-支付宝,2-微信,3-商福通渠道支付宝,4-商福通渠道微信
|
||||
Status int32 `gorm:"column:status;type:integer;not null;comment:订单状态:0-待支付,1-已支付,2-已取消,3-已退款" json:"status"` // 订单状态:0-待支付,1-已支付,2-已取消,3-已退款
|
||||
PayURL *string `gorm:"column:pay_url;type:text;comment:支付链接" json:"pay_url"` // 支付链接
|
||||
PaidAt *orm.LocalDateTime `gorm:"column:paid_at;type:timestamp without time zone;comment:支付时间" json:"paid_at"` // 支付时间
|
||||
@@ -33,6 +33,7 @@ type Trade struct {
|
||||
UpdatedAt *orm.LocalDateTime `gorm:"column:updated_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp without time zone;comment:删除时间" json:"deleted_at"` // 删除时间
|
||||
Acquirer int32 `gorm:"column:acquirer;type:integer;not null;comment:收单机构:1-支付宝,2-微信,3-银联" json:"acquirer"` // 收单机构:1-支付宝,2-微信,3-银联
|
||||
Platform int32 `gorm:"column:platform;type:integer;not null;comment:支付平台:1-电脑网站,2-手机网站" json:"platform"` // 支付平台:1-电脑网站,2-手机网站
|
||||
}
|
||||
|
||||
// TableName Trade's table name
|
||||
|
||||
@@ -45,6 +45,7 @@ func newTrade(db *gorm.DB, opts ...gen.DOOption) trade {
|
||||
_trade.UpdatedAt = field.NewField(tableName, "updated_at")
|
||||
_trade.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||
_trade.Acquirer = field.NewInt32(tableName, "acquirer")
|
||||
_trade.Platform = field.NewInt32(tableName, "platform")
|
||||
|
||||
_trade.fillFieldMap()
|
||||
|
||||
@@ -64,7 +65,7 @@ type trade struct {
|
||||
Remark field.String // 订单备注
|
||||
Amount field.Field // 订单总金额
|
||||
Payment field.Field // 支付金额
|
||||
Method field.Int32 // 支付方式:1-支付宝,2-微信,3-商福通
|
||||
Method field.Int32 // 支付方式:1-支付宝,2-微信,3-商福通渠道支付宝,4-商福通渠道微信
|
||||
Status field.Int32 // 订单状态:0-待支付,1-已支付,2-已取消,3-已退款
|
||||
PayURL field.String // 支付链接
|
||||
PaidAt field.Field // 支付时间
|
||||
@@ -73,6 +74,7 @@ type trade struct {
|
||||
UpdatedAt field.Field // 更新时间
|
||||
DeletedAt field.Field // 删除时间
|
||||
Acquirer field.Int32 // 收单机构:1-支付宝,2-微信,3-银联
|
||||
Platform field.Int32 // 支付平台:1-电脑网站,2-手机网站
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@@ -107,6 +109,7 @@ func (t *trade) updateTableName(table string) *trade {
|
||||
t.UpdatedAt = field.NewField(table, "updated_at")
|
||||
t.DeletedAt = field.NewField(table, "deleted_at")
|
||||
t.Acquirer = field.NewInt32(table, "acquirer")
|
||||
t.Platform = field.NewInt32(table, "platform")
|
||||
|
||||
t.fillFieldMap()
|
||||
|
||||
@@ -123,7 +126,7 @@ func (t *trade) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
func (t *trade) fillFieldMap() {
|
||||
t.fieldMap = make(map[string]field.Expr, 18)
|
||||
t.fieldMap = make(map[string]field.Expr, 19)
|
||||
t.fieldMap["id"] = t.ID
|
||||
t.fieldMap["user_id"] = t.UserID
|
||||
t.fieldMap["inner_no"] = t.InnerNo
|
||||
@@ -142,6 +145,7 @@ func (t *trade) fillFieldMap() {
|
||||
t.fieldMap["updated_at"] = t.UpdatedAt
|
||||
t.fieldMap["deleted_at"] = t.DeletedAt
|
||||
t.fieldMap["acquirer"] = t.Acquirer
|
||||
t.fieldMap["platform"] = t.Platform
|
||||
}
|
||||
|
||||
func (t trade) clone(db *gorm.DB) trade {
|
||||
|
||||
@@ -28,6 +28,8 @@ func ApplyRouters(app *fiber.App) {
|
||||
user.Post("/recharge/confirm/alipay", handlers.RechargeConfirmAlipay)
|
||||
user.Post("/recharge/prepare/wechat", handlers.RechargePrepareWechat)
|
||||
user.Post("/recharge/confirm/wechat", handlers.RechargeConfirmWechat)
|
||||
user.Post("/recharge/prepare", handlers.RechargePrepare)
|
||||
user.Post("/recharge/confirm", handlers.RechargeConfirm)
|
||||
|
||||
// 白名单
|
||||
whitelist := api.Group("/whitelist")
|
||||
|
||||
@@ -87,7 +87,7 @@ func (s *resourceService) CreateResource(uid int32, now time.Time, ser *CreateRe
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *resourceService) PrepareResource(uid int32, now time.Time, method trade2.Method, ser *CreateResourceSerializer) (*TradeCreateResult, error) {
|
||||
func (s *resourceService) PrepareResource(uid int32, now time.Time, ser *CreateResourceSerializer) (*TradeCreateResult, error) {
|
||||
|
||||
data, err := ser.ToData()
|
||||
if err != nil {
|
||||
@@ -97,18 +97,22 @@ func (s *resourceService) PrepareResource(uid int32, now time.Time, method trade
|
||||
name := data.GetName()
|
||||
amount := data.GetPrice()
|
||||
|
||||
method := ser.PaymentMethod
|
||||
platform := ser.PaymentPlatform
|
||||
|
||||
// 保存到数据库
|
||||
var result *TradeCreateResult
|
||||
err = q.Q.Transaction(func(q *q.Query) error {
|
||||
var err error
|
||||
|
||||
// 生成交易订单
|
||||
result, err = Trade.SendCreateTradeByQrcode(q, uid, now, &TradeCreateData{
|
||||
result, err = Trade.CreateTrade(q, uid, now, &TradeCreateData{
|
||||
Subject: "购买套餐 - " + name,
|
||||
Amount: amount,
|
||||
ExpireAt: time.Now().Add(30 * time.Minute),
|
||||
Type: trade2.TypeRecharge,
|
||||
Method: method,
|
||||
Platform: platform,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -124,7 +128,6 @@ func (s *resourceService) PrepareResource(uid int32, now time.Time, method trade
|
||||
Uid: uid,
|
||||
TradeId: result.Trade.ID,
|
||||
BillId: result.Bill.ID,
|
||||
Method: method,
|
||||
CreateResourceSerializer: resourceSerializer,
|
||||
}, 30*time.Minute).Err()
|
||||
if err != nil {
|
||||
@@ -158,9 +161,9 @@ func (s *resourceService) CompleteResource(tradeNo string, now time.Time, opResu
|
||||
rs = opResult[0]
|
||||
} else {
|
||||
var err error
|
||||
rs, err = Trade.VerifyCreateTrade(&TradeVerifyData{
|
||||
rs, err = Trade.VerifyTrade(&TradeVerifyData{
|
||||
TradeNo: tradeNo,
|
||||
Method: cache.Method,
|
||||
Method: cache.PaymentMethod,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -229,7 +232,7 @@ func (s *resourceService) CancelResource(tradeNo string, now time.Time, opRevoke
|
||||
|
||||
// 取消交易
|
||||
if len(opRevoked) <= 0 {
|
||||
err = Trade.CancelTrade(tradeNo, cache.Method)
|
||||
err = Trade.CancelTrade(tradeNo, cache.PaymentMethod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -403,9 +406,11 @@ func (data *CreateLongResourceData) GetPrice() decimal.Decimal {
|
||||
}
|
||||
|
||||
type CreateResourceSerializer struct {
|
||||
Type resource2.Type `json:"type" validate:"required"`
|
||||
Short *CreateShortResourceData `json:"short,omitempty"`
|
||||
Long *CreateLongResourceData `json:"long,omitempty"`
|
||||
Type resource2.Type `json:"type" validate:"required"`
|
||||
Short *CreateShortResourceData `json:"short,omitempty"`
|
||||
Long *CreateLongResourceData `json:"long,omitempty"`
|
||||
PaymentMethod trade2.Method `json:"payment_method" validate:"required"`
|
||||
PaymentPlatform trade2.Platform `json:"payment_platform" validate:"required"`
|
||||
}
|
||||
|
||||
func (s *CreateResourceSerializer) ToData() (CreateResourceData, error) {
|
||||
@@ -433,10 +438,9 @@ func (s *CreateResourceSerializer) ByData(data CreateResourceData) error {
|
||||
}
|
||||
|
||||
type CreateResourceCache struct {
|
||||
Uid int32 `json:"uid"`
|
||||
TradeId int32 `json:"trade_id"`
|
||||
BillId int32 `json:"bill_id"`
|
||||
Method trade2.Method `json:"method"`
|
||||
Uid int32 `json:"uid"`
|
||||
TradeId int32 `json:"trade_id"`
|
||||
BillId int32 `json:"bill_id"`
|
||||
*CreateResourceSerializer
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"platform/web/core"
|
||||
bill2 "platform/web/domains/bill"
|
||||
coupon2 "platform/web/domains/coupon"
|
||||
trade2 "platform/web/domains/trade"
|
||||
@@ -29,11 +30,12 @@ var Trade = &tradeService{}
|
||||
type tradeService struct {
|
||||
}
|
||||
|
||||
func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.Time, data *TradeCreateData) (*TradeCreateResult, error) {
|
||||
func (s *tradeService) CreateTrade(q *q.Query, uid int32, now time.Time, data *TradeCreateData) (*TradeCreateResult, error) {
|
||||
var subject = data.Subject
|
||||
var expire = data.ExpireAt
|
||||
var tType = data.Type
|
||||
var method = data.Method
|
||||
var platform = data.Platform
|
||||
var amount = data.Amount
|
||||
|
||||
// 实际支付金额,只在创建真实订单时使用
|
||||
@@ -103,11 +105,10 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
|
||||
|
||||
// 创建支付订单
|
||||
var payUrl string
|
||||
var acquirer trade2.Acquirer
|
||||
switch method {
|
||||
switch {
|
||||
|
||||
// 调用支付宝支付接口
|
||||
case trade2.MethodAlipay:
|
||||
// 支付宝 + 电脑网站
|
||||
case method == trade2.MethodAlipay && platform == trade2.PlatformDesktop:
|
||||
resp, err := g.Alipay.TradePagePay(alipay.TradePagePay{
|
||||
QRPayMode: "4",
|
||||
QRCodeWidth: "196", // 二维码宽度需要-4,支付宝页面布局有问题
|
||||
@@ -123,10 +124,9 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
|
||||
return nil, err
|
||||
}
|
||||
payUrl = resp.String()
|
||||
acquirer = trade2.AcquirerAlipay
|
||||
|
||||
// 调用微信支付接口
|
||||
case trade2.MethodWeChat:
|
||||
// 微信 + 电脑网站
|
||||
case method == trade2.MethodWeChat && platform == trade2.PlatformDesktop:
|
||||
resp, _, err := g.WechatPay.Native.Prepay(context.Background(), native.PrepayRequest{
|
||||
Appid: &env.WechatPayAppId,
|
||||
Mchid: &env.WechatPayMchId,
|
||||
@@ -142,10 +142,9 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
|
||||
return nil, err
|
||||
}
|
||||
payUrl = *resp.CodeUrl
|
||||
acquirer = trade2.AcquirerWeChat
|
||||
|
||||
// 调用商福通接口
|
||||
case trade2.MethodSft:
|
||||
// 商福通 + 电脑网站
|
||||
case method == trade2.MethodSft && platform == trade2.PlatformDesktop:
|
||||
resp, err := g.SFTPay.PaymentScanPay(&g.PaymentScanPayReq{
|
||||
MchOrderNo: tradeNo,
|
||||
Subject: subject,
|
||||
@@ -159,155 +158,9 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
|
||||
return nil, err
|
||||
}
|
||||
payUrl = u.Z(u.Z(resp.PayInfo).QrCodeUrl)
|
||||
if payUrl == "" {
|
||||
return nil, errors.New("支付接口未返回正确的二维码地址")
|
||||
}
|
||||
switch resp.PayType {
|
||||
case g.SftAlipay:
|
||||
acquirer = trade2.AcquirerAlipay
|
||||
case g.SftWeChat:
|
||||
acquirer = trade2.AcquirerWeChat
|
||||
case g.SftUnionPay:
|
||||
acquirer = trade2.AcquirerUnionPay
|
||||
}
|
||||
|
||||
// 不支持的支付方式
|
||||
default:
|
||||
return nil, ErrTransactionNotSupported
|
||||
}
|
||||
|
||||
// 保存交易订单
|
||||
var billType bill2.Type
|
||||
switch tType {
|
||||
case trade2.TypeRecharge:
|
||||
billType = bill2.TypeRecharge
|
||||
case trade2.TypePurchase:
|
||||
billType = bill2.TypeConsume
|
||||
}
|
||||
|
||||
var trade = m.Trade{
|
||||
UserID: uid,
|
||||
InnerNo: tradeNo,
|
||||
Subject: subject,
|
||||
Method: int32(method),
|
||||
Type: int32(tType),
|
||||
Amount: amount,
|
||||
PayURL: &payUrl,
|
||||
Acquirer: int32(acquirer),
|
||||
}
|
||||
err = q.Trade.Create(&trade)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存用户帐单
|
||||
var bill = m.Bill{
|
||||
BillNo: ID.GenReadable("bil"),
|
||||
UserID: uid,
|
||||
TradeID: &trade.ID,
|
||||
Info: &subject,
|
||||
Type: int32(billType),
|
||||
Amount: amount,
|
||||
}
|
||||
err = q.Bill.
|
||||
Omit(q.Bill.ResourceID, q.Bill.RefundID).
|
||||
Create(&bill)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 提交异步任务更新订单状态
|
||||
_, err = g.Asynq.Enqueue(tasks.NewUpdateTrade(tradeNo, method))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TradeCreateResult{
|
||||
TradeNo: tradeNo,
|
||||
PayURL: payUrl,
|
||||
Bill: &bill,
|
||||
Trade: &trade,
|
||||
}, nil
|
||||
}
|
||||
func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time.Time, data *TradeCreateData) (*TradeCreateResult, error) {
|
||||
var subject = data.Subject
|
||||
var expire = data.ExpireAt
|
||||
var tType = data.Type
|
||||
var method = data.Method
|
||||
var amount = data.Amount
|
||||
|
||||
// 实际支付金额,只在创建真实订单时使用
|
||||
var amountReal = data.Amount
|
||||
if env.RunMode == "debug" {
|
||||
amountReal = decimal.NewFromFloat(0.01)
|
||||
}
|
||||
|
||||
// 附加优惠券
|
||||
if data.CouponCode != "" {
|
||||
coupon, err := q.Coupon.
|
||||
Where(
|
||||
q.Coupon.Code.Eq(data.CouponCode),
|
||||
q.Coupon.Status.Eq(int32(coupon2.StatusUnused)),
|
||||
).
|
||||
Take()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("优惠券不存在或已失效")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var expireAt = time.Time(u.Z(coupon.ExpireAt))
|
||||
if !expireAt.IsZero() && expireAt.Before(now) {
|
||||
_, err = q.Coupon.
|
||||
Where(q.Coupon.ID.Eq(coupon.ID)).
|
||||
Update(q.Coupon.Status, coupon2.StatusExpired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.New("优惠券已过期")
|
||||
}
|
||||
|
||||
if amount.Cmp(coupon.MinAmount) < 0 {
|
||||
return nil, errors.New("订单金额未达到使用优惠券的条件")
|
||||
}
|
||||
|
||||
if coupon.UserID != nil {
|
||||
switch *coupon.UserID {
|
||||
// 指定用户的优惠券
|
||||
case uid:
|
||||
amount = amount.Sub(coupon.Amount)
|
||||
if expireAt.IsZero() {
|
||||
_, err = q.Coupon.
|
||||
Where(q.Coupon.ID.Eq(coupon.ID)).
|
||||
Update(q.Coupon.Status, int32(coupon2.StatusUsed))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// 该优惠券不属于当前用户
|
||||
default:
|
||||
return nil, errors.New("优惠券不属于当前用户")
|
||||
}
|
||||
} else {
|
||||
// 公开优惠券
|
||||
amount = amount.Sub(coupon.Amount)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
tradeNo, err := ID.GenSerial(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建支付订单
|
||||
var payUrl string
|
||||
var acquirer trade2.Acquirer
|
||||
switch method {
|
||||
|
||||
// 调用商福通接口
|
||||
case trade2.MethodSftAlipay, trade2.MethodSftWeChat:
|
||||
// 商福通 + 手机网站
|
||||
case (method == trade2.MethodSftAlipay || method == trade2.MethodSftWeChat) && platform == trade2.PlatformMobile:
|
||||
var payType g.SftPayType
|
||||
if method == trade2.MethodSftAlipay {
|
||||
payType = g.SftAlipay
|
||||
@@ -328,24 +181,31 @@ func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time
|
||||
return nil, err
|
||||
}
|
||||
payUrl = u.Z(u.Z(resp.PayInfo).PayUrl)
|
||||
if payUrl == "" {
|
||||
return nil, errors.New("支付接口未返回正确的二维码地址")
|
||||
}
|
||||
switch resp.PayType {
|
||||
case g.SftAlipay:
|
||||
acquirer = trade2.AcquirerAlipay
|
||||
case g.SftWeChat:
|
||||
acquirer = trade2.AcquirerWeChat
|
||||
case g.SftUnionPay:
|
||||
acquirer = trade2.AcquirerUnionPay
|
||||
}
|
||||
|
||||
// 不支持的支付方式
|
||||
default:
|
||||
slog.Warn(ErrTransactionNotSupported.Error(), "method", method, "platform", platform)
|
||||
return nil, ErrTransactionNotSupported
|
||||
}
|
||||
|
||||
// 保存交易订单
|
||||
var trade = m.Trade{
|
||||
UserID: uid,
|
||||
InnerNo: tradeNo,
|
||||
Subject: subject,
|
||||
Type: int32(tType),
|
||||
Method: int32(method),
|
||||
Platform: int32(platform),
|
||||
Amount: amount,
|
||||
PayURL: &payUrl,
|
||||
}
|
||||
|
||||
err = q.Trade.Create(&trade)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存用户帐单
|
||||
var billType bill2.Type
|
||||
switch tType {
|
||||
case trade2.TypeRecharge:
|
||||
@@ -354,22 +214,6 @@ func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time
|
||||
billType = bill2.TypeConsume
|
||||
}
|
||||
|
||||
var trade = m.Trade{
|
||||
UserID: uid,
|
||||
InnerNo: tradeNo,
|
||||
Subject: subject,
|
||||
Method: int32(method),
|
||||
Type: int32(tType),
|
||||
Amount: amount,
|
||||
PayURL: &payUrl,
|
||||
Acquirer: int32(acquirer),
|
||||
}
|
||||
err = q.Trade.Create(&trade)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存用户帐单
|
||||
var bill = m.Bill{
|
||||
BillNo: ID.GenReadable("bil"),
|
||||
UserID: uid,
|
||||
@@ -378,6 +222,7 @@ func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time
|
||||
Type: int32(billType),
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
err = q.Bill.
|
||||
Omit(q.Bill.ResourceID, q.Bill.RefundID).
|
||||
Create(&bill)
|
||||
@@ -398,78 +243,12 @@ func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time
|
||||
Trade: &trade,
|
||||
}, nil
|
||||
}
|
||||
func (s *tradeService) VerifyCreateTrade(data *TradeVerifyData) (*TradeSuccessResult, error) {
|
||||
var tradeNo = data.TradeNo
|
||||
var method = data.Method
|
||||
|
||||
// 检查交易号是否存在
|
||||
var transId string
|
||||
var paidAt time.Time
|
||||
var payment decimal.Decimal
|
||||
switch method {
|
||||
|
||||
// 检查支付宝交易
|
||||
case trade2.MethodAlipay:
|
||||
resp, err := g.Alipay.TradeQuery(context.Background(), alipay.TradeQuery{
|
||||
OutTradeNo: tradeNo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != alipay.CodeSuccess {
|
||||
slog.Warn("支付宝交易查询失败", "code", resp.Code, "sub_code", resp.SubCode, "msg", resp.Msg)
|
||||
return nil, errors.New("交易查询失败")
|
||||
}
|
||||
if resp.TradeStatus != alipay.TradeStatusSuccess {
|
||||
return nil, ErrTransactionNotPaid
|
||||
}
|
||||
|
||||
transId = resp.TradeNo
|
||||
payment, err = decimal.NewFromString(resp.TotalAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paidAt, err = time.Parse("2006-01-02 15:04:05", resp.SendPayDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查微信交易
|
||||
case trade2.MethodWeChat:
|
||||
resp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(context.Background(), native.QueryOrderByOutTradeNoRequest{
|
||||
OutTradeNo: &tradeNo,
|
||||
Mchid: &env.WechatPayMchId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if *resp.TradeState != "SUCCESS" {
|
||||
return nil, ErrTransactionNotPaid
|
||||
}
|
||||
|
||||
transId = *resp.TransactionId
|
||||
payment = decimal.NewFromInt(*resp.Amount.PayerTotal).Div(decimal.NewFromInt(100))
|
||||
paidAt, err = time.Parse(time.RFC3339, *resp.SuccessTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 不支持的支付方式
|
||||
default:
|
||||
return nil, ErrTransactionNotSupported
|
||||
}
|
||||
|
||||
return &TradeSuccessResult{
|
||||
TransId: transId,
|
||||
Payment: payment,
|
||||
Time: paidAt,
|
||||
}, nil
|
||||
}
|
||||
func (s *tradeService) OnTradeCreated(q *q.Query, data *OnTradeCreateData) (*m.Trade, error) {
|
||||
var transId = data.TransId
|
||||
var tradeNo = data.TradeNo
|
||||
var payment = data.Payment
|
||||
var paidAt = data.Time
|
||||
var acquirer = data.Acquirer
|
||||
|
||||
// 获取交易信息
|
||||
trade, err := q.Trade.
|
||||
@@ -491,6 +270,7 @@ func (s *tradeService) OnTradeCreated(q *q.Query, data *OnTradeCreateData) (*m.T
|
||||
trade.Status = int32(trade2.StatusSuccess)
|
||||
trade.OuterNo = &transId
|
||||
trade.Payment = payment
|
||||
trade.Acquirer = int32(acquirer)
|
||||
trade.PaidAt = u.P(orm.LocalDateTime(paidAt))
|
||||
trade.PayURL = u.P("")
|
||||
_, err = q.Trade.Updates(trade)
|
||||
@@ -574,12 +354,106 @@ func (s *tradeService) OnTradeRefunded(q *q.Query, tradeNo string, now time.Time
|
||||
panic("todo")
|
||||
}
|
||||
|
||||
func (s *tradeService) VerifyTrade(data *TradeVerifyData) (*TradeSuccessResult, error) {
|
||||
var tradeNo = data.TradeNo
|
||||
var method = data.Method
|
||||
|
||||
// 检查交易号是否存在
|
||||
var transId string
|
||||
var paidAt time.Time
|
||||
var payment decimal.Decimal
|
||||
switch method {
|
||||
|
||||
// 检查支付宝交易
|
||||
case trade2.MethodAlipay:
|
||||
resp, err := g.Alipay.TradeQuery(context.Background(), alipay.TradeQuery{
|
||||
OutTradeNo: tradeNo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != alipay.CodeSuccess {
|
||||
slog.Warn("支付宝交易查询失败", "code", resp.Code, "sub_code", resp.SubCode, "msg", resp.Msg)
|
||||
return nil, errors.New("交易查询失败")
|
||||
}
|
||||
if resp.TradeStatus != alipay.TradeStatusSuccess {
|
||||
return nil, ErrTransactionNotPaid
|
||||
}
|
||||
|
||||
transId = resp.TradeNo
|
||||
payment, err = decimal.NewFromString(resp.TotalAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paidAt, err = time.Parse("2006-01-02 15:04:05", resp.SendPayDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查微信交易
|
||||
case trade2.MethodWeChat:
|
||||
resp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(context.Background(), native.QueryOrderByOutTradeNoRequest{
|
||||
OutTradeNo: &tradeNo,
|
||||
Mchid: &env.WechatPayMchId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if *resp.TradeState != "SUCCESS" {
|
||||
return nil, ErrTransactionNotPaid
|
||||
}
|
||||
|
||||
transId = *resp.TransactionId
|
||||
payment = decimal.NewFromInt(*resp.Amount.PayerTotal).Div(decimal.NewFromInt(100))
|
||||
paidAt, err = time.Parse(time.RFC3339, *resp.SuccessTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查商福通交易
|
||||
case trade2.MethodSft, trade2.MethodSftAlipay, trade2.MethodSftWeChat:
|
||||
resp, err := g.SFTPay.QueryTrade(&g.QueryTradeReq{
|
||||
MchOrderNo: &tradeNo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.State != g.SftTradeSuccess {
|
||||
return nil, ErrTransactionNotPaid
|
||||
}
|
||||
|
||||
if resp.PayOrderId == nil {
|
||||
return nil, errors.New("商福通交易号不存在")
|
||||
}
|
||||
if resp.PayTime == nil {
|
||||
return nil, errors.New("商福通交易时间不存在")
|
||||
}
|
||||
transId = *resp.PayOrderId
|
||||
payment = decimal.NewFromInt(resp.Amount).Div(decimal.NewFromInt(100))
|
||||
paidAt, err = time.Parse("2006-01-02 15:04:05", *resp.PayTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 不支持的支付方式
|
||||
default:
|
||||
return nil, ErrTransactionNotSupported
|
||||
}
|
||||
|
||||
return &TradeSuccessResult{
|
||||
TransId: transId,
|
||||
Payment: payment,
|
||||
Time: paidAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type TradeCreateData struct {
|
||||
Subject string
|
||||
Amount decimal.Decimal
|
||||
ExpireAt time.Time
|
||||
Type trade2.Type
|
||||
Method trade2.Method
|
||||
Platform trade2.Platform
|
||||
CouponCode string
|
||||
}
|
||||
|
||||
@@ -596,9 +470,10 @@ type TradeVerifyData struct {
|
||||
}
|
||||
|
||||
type TradeSuccessResult struct {
|
||||
TransId string
|
||||
Payment decimal.Decimal
|
||||
Time time.Time
|
||||
TransId string
|
||||
Payment decimal.Decimal
|
||||
Time time.Time
|
||||
Acquirer trade2.Acquirer
|
||||
}
|
||||
|
||||
type OnTradeCreateData struct {
|
||||
@@ -606,13 +481,6 @@ type OnTradeCreateData struct {
|
||||
TradeSuccessResult
|
||||
}
|
||||
|
||||
type TradePlatform int
|
||||
|
||||
const (
|
||||
TradePlatformDesktop TradePlatform = iota + 1 // 桌面端
|
||||
TradePlatformMobile // 移动端
|
||||
)
|
||||
|
||||
type TradeErr string
|
||||
|
||||
func (e TradeErr) Error() string {
|
||||
@@ -620,6 +488,6 @@ func (e TradeErr) Error() string {
|
||||
}
|
||||
|
||||
var (
|
||||
ErrTransactionNotPaid = TradeErr("交易未支付")
|
||||
ErrTransactionNotSupported = TradeErr("不支持的支付方式")
|
||||
ErrTransactionNotPaid = core.NewBizErr("交易未支付")
|
||||
ErrTransactionNotSupported = core.NewBizErr("不支持的支付方式")
|
||||
)
|
||||
|
||||
78
web/web.go
78
web/web.go
@@ -126,50 +126,52 @@ func newLogger() fiber.Handler {
|
||||
return false
|
||||
},
|
||||
Done: func(c *fiber.Ctx, logBytes []byte) {
|
||||
var logStr = strings.TrimPrefix(string(logBytes), "🚀")
|
||||
var logVars = strings.Split(logStr, "|")
|
||||
go func(ip, ua, method, path string, status int, logBytes []byte) {
|
||||
var logStr = strings.TrimPrefix(string(logBytes), "🚀")
|
||||
var logVars = strings.Split(logStr, "|")
|
||||
|
||||
var reqTimeStr = strings.TrimSpace(logVars[0])
|
||||
reqTime, err := time.ParseInLocation("2006-01-02 15:04:05", reqTimeStr, time.Local)
|
||||
if err != nil {
|
||||
slog.Error("时间解析错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
var reqTimeStr = strings.TrimSpace(logVars[0])
|
||||
reqTime, err := time.ParseInLocation("2006-01-02 15:04:05", reqTimeStr, time.Local)
|
||||
if err != nil {
|
||||
slog.Error("时间解析错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
var authInfo = strings.Split(strings.TrimSpace(logVars[1]), " ")
|
||||
var authType = auth.PayloadTypeFromStr(strings.TrimSpace(authInfo[0]))
|
||||
authID, err := strconv.Atoi(strings.TrimSpace(authInfo[1]))
|
||||
if err != nil {
|
||||
slog.Error("负载ID解析错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
var authInfo = strings.Split(strings.TrimSpace(logVars[1]), " ")
|
||||
var authType = auth.PayloadTypeFromStr(strings.TrimSpace(authInfo[0]))
|
||||
authID, err := strconv.Atoi(strings.TrimSpace(authInfo[1]))
|
||||
if err != nil {
|
||||
slog.Error("负载ID解析错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
var latency = strings.TrimSpace(logVars[4])
|
||||
var latency = strings.TrimSpace(logVars[4])
|
||||
|
||||
var errStr = strings.TrimSpace(logVars[5])
|
||||
var errStr = strings.TrimSpace(logVars[5])
|
||||
|
||||
var item = &m.LogsRequest{
|
||||
IP: c.IP(),
|
||||
Ua: u.P(c.Get("User-Agent")),
|
||||
Method: c.Method(),
|
||||
Path: c.Path(),
|
||||
Latency: &latency,
|
||||
Status: int32(c.Response().StatusCode()),
|
||||
Error: &errStr,
|
||||
Time: u.P(orm.LocalDateTime(reqTime)),
|
||||
}
|
||||
if authType != auth.PayloadNone {
|
||||
item.Identity = u.P(int32(authType))
|
||||
}
|
||||
if authID != 0 {
|
||||
item.Visitor = u.P(int32(authID))
|
||||
}
|
||||
var item = &m.LogsRequest{
|
||||
IP: ip,
|
||||
Ua: u.P(ua),
|
||||
Method: method,
|
||||
Path: path,
|
||||
Latency: &latency,
|
||||
Status: int32(status),
|
||||
Error: &errStr,
|
||||
Time: u.P(orm.LocalDateTime(reqTime)),
|
||||
}
|
||||
if authType != auth.PayloadNone {
|
||||
item.Identity = u.P(int32(authType))
|
||||
}
|
||||
if authID != 0 {
|
||||
item.Visitor = u.P(int32(authID))
|
||||
}
|
||||
|
||||
err = q.LogsRequest.Create(item)
|
||||
if err != nil {
|
||||
slog.Error("日志记录错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
err = q.LogsRequest.Create(item)
|
||||
if err != nil {
|
||||
slog.Error("日志记录错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
}(c.IP(), c.Get("User-Agent"), c.Method(), c.Path(), c.Response().StatusCode(), logBytes)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user