diff --git a/miniprogram/pages/withdraw-records/withdraw-records.js b/miniprogram/pages/withdraw-records/withdraw-records.js index 9b851f9f..5ef1c2fd 100644 --- a/miniprogram/pages/withdraw-records/withdraw-records.js +++ b/miniprogram/pages/withdraw-records/withdraw-records.js @@ -34,7 +34,8 @@ Page({ amount: (item.amount != null ? item.amount : 0).toFixed(2), status: this.statusText(item.status), statusRaw: item.status, - createdAt: (item.createdAt ?? item.created_at) ? this.formatDate(item.createdAt ?? item.created_at) : '--' + createdAt: (item.createdAt ?? item.created_at) ? this.formatDate(item.createdAt ?? item.created_at) : '--', + canReceive: !!item.canReceive })) this.setData({ list, loading: false }) } else { @@ -68,5 +69,55 @@ Page({ goBack() { wx.navigateBack() + }, + + async onReceiveTap(e) { + const id = e.currentTarget.dataset.id + if (!id) return + wx.showLoading({ title: '加载中...' }) + try { + const res = await app.request('/api/miniprogram/withdraw/confirm-info?id=' + encodeURIComponent(id)) + wx.hideLoading() + if (!res || !res.success || !res.data) { + wx.showToast({ title: res?.message || '获取领取信息失败', icon: 'none' }) + return + } + const { mchId, appId, package: pkg } = res.data + if (!pkg || pkg === '') { + wx.showToast({ + title: '打款已发起,请到微信零钱中查看', + icon: 'none', + duration: 2500 + }) + return + } + if (!wx.canIUse('requestMerchantTransfer')) { + wx.showToast({ title: '当前微信版本过低,请更新后重试', icon: 'none' }) + return + } + wx.requestMerchantTransfer({ + mchId: mchId || '', + appId: appId || wx.getAccountInfoSync().miniProgram.appId, + package: pkg, + success: (res) => { + if (res.errMsg === 'requestMerchantTransfer:ok') { + wx.showToast({ title: '已调起收款页', icon: 'success' }) + this.loadRecords() + } else { + wx.showToast({ title: res.errMsg || '操作完成', icon: 'none' }) + } + }, + fail: (err) => { + if (err.errMsg && err.errMsg.indexOf('cancel') !== -1) { + wx.showToast({ title: '已取消', icon: 'none' }) + } else { + wx.showToast({ title: err.errMsg || '调起失败', icon: 'none' }) + } + } + }) + } catch (e) { + wx.hideLoading() + wx.showToast({ title: '网络异常,请重试', icon: 'none' }) + } } }) diff --git a/miniprogram/pages/withdraw-records/withdraw-records.wxml b/miniprogram/pages/withdraw-records/withdraw-records.wxml index a4e9e9e0..def185dc 100644 --- a/miniprogram/pages/withdraw-records/withdraw-records.wxml +++ b/miniprogram/pages/withdraw-records/withdraw-records.wxml @@ -15,7 +15,10 @@ ¥{{item.amount}} {{item.createdAt}} - {{item.status}} + + {{item.status}} + + diff --git a/miniprogram/pages/withdraw-records/withdraw-records.wxss b/miniprogram/pages/withdraw-records/withdraw-records.wxss index a70da91a..4de9e2bb 100644 --- a/miniprogram/pages/withdraw-records/withdraw-records.wxss +++ b/miniprogram/pages/withdraw-records/withdraw-records.wxss @@ -48,10 +48,24 @@ } .item:last-child { border-bottom: none; } .item-left { display: flex; flex-direction: column; gap: 8rpx; } +.item-right { display: flex; flex-direction: column; align-items: flex-end; gap: 12rpx; } .amount { font-size: 32rpx; font-weight: 600; color: #fff; } .time { font-size: 24rpx; color: rgba(255,255,255,0.5); } .status { font-size: 26rpx; } .status.status-pending { color: #FFA500; } +.status.status-processing { color: #4CAF50; } .status.status-pending_confirm { color: #4CAF50; } .status.status-success { color: #4CAF50; } .status.status-failed { color: rgba(255,255,255,0.5); } +.btn-receive { + margin: 0; + padding: 0 24rpx; + height: 56rpx; + line-height: 56rpx; + font-size: 24rpx; + color: #00CED1; + background: transparent; + border: 2rpx solid #00CED1; + border-radius: 8rpx; +} +.btn-receive::after { border: none; } diff --git a/soul-admin/src/pages/distribution/DistributionPage.tsx b/soul-admin/src/pages/distribution/DistributionPage.tsx index 79617342..2c3e3a8f 100644 --- a/soul-admin/src/pages/distribution/DistributionPage.tsx +++ b/soul-admin/src/pages/distribution/DistributionPage.tsx @@ -138,8 +138,8 @@ export function DistributionPage() { } } - async function loadTabData(tab: string) { - if (loadedTabs.has(tab)) return + async function loadTabData(tab: string, force = false) { + if (!force && loadedTabs.has(tab)) return setLoading(true) try { const usersArr = users @@ -228,7 +228,7 @@ export function DistributionPage() { return next }) if (activeTab === 'overview') loadInitialData() - loadTabData(activeTab) + loadTabData(activeTab, true) } async function handleApproveWithdrawal(id: string) { diff --git a/soul-api/go.mod b/soul-api/go.mod index 1ec3255f..a6b1db88 100644 --- a/soul-api/go.mod +++ b/soul-api/go.mod @@ -3,6 +3,8 @@ module soul-api go 1.25 require ( + github.com/ArtisanCloud/PowerLibs/v3 v3.3.2 + github.com/ArtisanCloud/PowerWeChat/v3 v3.4.38 github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.10.0 github.com/joho/godotenv v1.5.1 @@ -13,9 +15,6 @@ require ( ) require ( - github.com/ArtisanCloud/PowerLibs/v3 v3.3.2 // indirect - github.com/ArtisanCloud/PowerSocialite/v3 v3.0.9 // indirect - github.com/ArtisanCloud/PowerWeChat/v3 v3.4.38 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -34,7 +33,6 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -45,7 +43,6 @@ require ( github.com/redis/go-redis/v9 v9.17.3 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - github.com/wechatpay-apiv3/wechatpay-go v0.2.21 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/soul-api/go.sum b/soul-api/go.sum index d8353fbd..63d6cc20 100644 --- a/soul-api/go.sum +++ b/soul-api/go.sum @@ -1,10 +1,11 @@ github.com/ArtisanCloud/PowerLibs/v3 v3.3.2 h1:IInr1YWwkhwOykxDqux1Goym0uFhrYwBjmgLnEwCLqs= github.com/ArtisanCloud/PowerLibs/v3 v3.3.2/go.mod h1:xFGsskCnzAu+6rFEJbGVAlwhrwZPXAny6m7j71S/B5k= -github.com/ArtisanCloud/PowerSocialite/v3 v3.0.9 h1:ItdVnpav2gmYdf3kM9wiXXoQwn+FVTYDZx0ZA/Ee48I= -github.com/ArtisanCloud/PowerSocialite/v3 v3.0.9/go.mod h1:VZQNCvcK/rldF3QaExiSl1gJEAkyc5/I8RLOd3WFZq4= github.com/ArtisanCloud/PowerWeChat/v3 v3.4.38 h1:yu4A7WhPXfs/RSYFL2UdHFRQYAXbrpiBOT3kJ5hjepU= github.com/ArtisanCloud/PowerWeChat/v3 v3.4.38/go.mod h1:boWl2cwbgXt1AbrYTWMXs9Ebby6ecbJ1CyNVRaNVqUY= -github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -17,7 +18,6 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -31,6 +31,9 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -44,8 +47,8 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= @@ -94,20 +97,27 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= -github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs= -github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.4.0 h1:LJE4SW3jd4lQTESnlpQZcBhQ3oci0U2MLR5uhicfTHQ= +go.opentelemetry.io/otel/sdk v1.4.0/go.mod h1:71GJPNJh4Qju6zJuYl1CrYtXbrgfau/M9UAggqiy1UE= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= @@ -115,28 +125,18 @@ go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/soul-api/internal/handler/admin_withdrawals.go b/soul-api/internal/handler/admin_withdrawals.go index 14d4bd19..c354713f 100644 --- a/soul-api/internal/handler/admin_withdrawals.go +++ b/soul-api/internal/handler/admin_withdrawals.go @@ -211,10 +211,14 @@ func AdminWithdrawalsAction(c *gin.Context) { "batch_id": batchID, "processed_at": now, }).Error + // 始终返回 out_batch_no 便于追踪;batch_id 为微信返回,可能为空 c.JSON(http.StatusOK, gin.H{ "success": true, "message": "已发起打款,微信处理中", - "data": gin.H{"batch_id": batchID}, + "data": gin.H{ + "batch_id": batchID, + "out_batch_no": outBatchNo, + }, }) return diff --git a/soul-api/internal/handler/miniprogram.go b/soul-api/internal/handler/miniprogram.go index c2bdfe83..175835a4 100644 --- a/soul-api/internal/handler/miniprogram.go +++ b/soul-api/internal/handler/miniprogram.go @@ -3,6 +3,7 @@ package handler import ( "encoding/json" "fmt" + "io" "net/http" "strings" "time" @@ -287,33 +288,19 @@ func miniprogramPayPost(c *gin.Context) { fmt.Printf("[MiniprogramPay] 插入订单失败: %v\n", err) } - // 调用微信统一下单 - params := map[string]string{ - "body": description, - "out_trade_no": orderSn, - "total_fee": fmt.Sprintf("%d", totalFee), - "spbill_create_ip": clientIP, - "notify_url": "https://soul.quwanzhi.com/api/miniprogram/pay/notify", - "trade_type": "JSAPI", - "openid": req.OpenID, - "attach": fmt.Sprintf(`{"productType":"%s","productId":"%s","userId":"%s"}`, req.ProductType, req.ProductID, userID), - } - - result, err := wechat.PayV2UnifiedOrder(params) + attach := fmt.Sprintf(`{"productType":"%s","productId":"%s","userId":"%s"}`, req.ProductType, req.ProductID, userID) + ctx := c.Request.Context() + prepayID, err := wechat.PayJSAPIOrder(ctx, req.OpenID, orderSn, totalFee, description, attach) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": fmt.Sprintf("微信支付请求失败: %v", err)}) + c.JSON(http.StatusOK, gin.H{"success": false, "error": fmt.Sprintf("微信支付请求失败: %v", err)}) return } - - prepayID := result["prepay_id"] - if prepayID == "" { - c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "微信支付返回数据异常"}) + payParams, err := wechat.GetJSAPIPayParams(prepayID) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "error": fmt.Sprintf("生成支付参数失败: %v", err)}) return } - // 生成小程序支付参数 - payParams := wechat.GenerateJSAPIPayParams(prepayID) - c.JSON(http.StatusOK, gin.H{ "success": true, "data": map[string]interface{}{ @@ -332,7 +319,8 @@ func miniprogramPayGet(c *gin.Context) { return } - result, err := wechat.PayV2OrderQuery(orderSn) + ctx := c.Request.Context() + tradeState, transactionID, totalFee, err := wechat.QueryOrderByOutTradeNo(ctx, orderSn) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": true, @@ -344,10 +332,7 @@ func miniprogramPayGet(c *gin.Context) { return } - // 映射微信支付状态 - tradeState := result["trade_state"] status := "paying" - switch tradeState { case "SUCCESS": status = "paid" @@ -357,175 +342,130 @@ func miniprogramPayGet(c *gin.Context) { status = "refunded" } - totalFee := 0 - if result["total_fee"] != "" { - fmt.Sscanf(result["total_fee"], "%d", &totalFee) - } - c.JSON(http.StatusOK, gin.H{ "success": true, "data": map[string]interface{}{ "status": status, "orderSn": orderSn, - "transactionId": result["transaction_id"], + "transactionId": transactionID, "totalFee": totalFee, }, }) } -// MiniprogramPayNotify POST /api/miniprogram/pay/notify +// MiniprogramPayNotify POST /api/miniprogram/pay/notify(v3 支付回调,PowerWeChat 验签解密) func MiniprogramPayNotify(c *gin.Context) { - // 读取 XML body - body, err := c.GetRawData() - if err != nil { - c.String(http.StatusBadRequest, failResponse()) - return - } + resp, err := wechat.HandlePayNotify(c.Request, func(orderSn, transactionID string, totalFee int, attachStr, openID string) error { + totalAmount := float64(totalFee) / 100 + fmt.Printf("[PayNotify] 支付成功: orderSn=%s, transactionId=%s, amount=%.2f\n", orderSn, transactionID, totalAmount) - // 解析 XML - data := wechat.XMLToMap(string(body)) - - // 验证签名 - if !wechat.VerifyPayNotify(data) { - fmt.Println("[PayNotify] 签名验证失败") + var attach struct { + ProductType string `json:"productType"` + ProductID string `json:"productId"` + UserID string `json:"userId"` + } + if attachStr != "" { + _ = json.Unmarshal([]byte(attachStr), &attach) + } + + db := database.DB() + buyerUserID := attach.UserID + if openID != "" { + var user model.User + if err := db.Where("open_id = ?", openID).First(&user).Error; err == nil { + if attach.UserID != "" && user.ID != attach.UserID { + fmt.Printf("[PayNotify] 买家身份校验: attach.userId 与 openId 解析不一致,以 openId 为准\n") + } + buyerUserID = user.ID + } + } + if buyerUserID == "" && attach.UserID != "" { + buyerUserID = attach.UserID + } + + var order model.Order + result := db.Where("order_sn = ?", orderSn).First(&order) + if result.Error != nil { + fmt.Printf("[PayNotify] 订单不存在,补记订单: %s\n", orderSn) + productID := attach.ProductID + if productID == "" { + productID = "fullbook" + } + productType := attach.ProductType + if productType == "" { + productType = "unknown" + } + desc := "支付回调补记订单" + status := "paid" + now := time.Now() + order = model.Order{ + ID: orderSn, + OrderSN: orderSn, + UserID: buyerUserID, + OpenID: openID, + ProductType: productType, + ProductID: &productID, + Amount: totalAmount, + Description: &desc, + Status: &status, + TransactionID: &transactionID, + PayTime: &now, + } + db.Create(&order) + } else if *order.Status != "paid" { + status := "paid" + now := time.Now() + db.Model(&order).Updates(map[string]interface{}{ + "status": status, + "transaction_id": transactionID, + "pay_time": now, + }) + fmt.Printf("[PayNotify] 订单状态已更新为已支付: %s\n", orderSn) + } else { + fmt.Printf("[PayNotify] 订单已支付,跳过更新: %s\n", orderSn) + } + + if buyerUserID != "" && attach.ProductType != "" { + if attach.ProductType == "fullbook" { + db.Model(&model.User{}).Where("id = ?", buyerUserID).Update("has_full_book", true) + fmt.Printf("[PayNotify] 用户已购全书: %s\n", buyerUserID) + } else if attach.ProductType == "section" && attach.ProductID != "" { + var count int64 + db.Model(&model.Order{}).Where( + "user_id = ? AND product_type = 'section' AND product_id = ? AND status = 'paid' AND order_sn != ?", + buyerUserID, attach.ProductID, orderSn, + ).Count(&count) + if count == 0 { + fmt.Printf("[PayNotify] 用户首次购买章节: %s - %s\n", buyerUserID, attach.ProductID) + } else { + fmt.Printf("[PayNotify] 用户已有该章节的其他已支付订单: %s - %s\n", buyerUserID, attach.ProductID) + } + } + productID := attach.ProductID + if productID == "" { + productID = "fullbook" + } + db.Where( + "user_id = ? AND product_type = ? AND product_id = ? AND status = 'created' AND order_sn != ?", + buyerUserID, attach.ProductType, productID, orderSn, + ).Delete(&model.Order{}) + processReferralCommission(db, buyerUserID, totalAmount, orderSn) + } + return nil + }) + if err != nil { + fmt.Printf("[PayNotify] 处理回调失败: %v\n", err) c.String(http.StatusOK, failResponse()) return } - - // 检查支付结果 - if data["return_code"] != "SUCCESS" || data["result_code"] != "SUCCESS" { - fmt.Printf("[PayNotify] 支付未成功: %s\n", data["err_code"]) - c.String(http.StatusOK, successResponse()) - return - } - - orderSn := data["out_trade_no"] - transactionID := data["transaction_id"] - totalFee := 0 - fmt.Sscanf(data["total_fee"], "%d", &totalFee) - totalAmount := float64(totalFee) / 100 - openID := data["openid"] - - fmt.Printf("[PayNotify] 支付成功: orderSn=%s, transactionId=%s, amount=%.2f\n", orderSn, transactionID, totalAmount) - - // 解析附加数据 - var attach struct { - ProductType string `json:"productType"` - ProductID string `json:"productId"` - UserID string `json:"userId"` - } - if data["attach"] != "" { - json.Unmarshal([]byte(data["attach"]), &attach) - } - - db := database.DB() - - // 用 openID 解析真实买家身份 - buyerUserID := attach.UserID - if openID != "" { - var user model.User - if err := db.Where("open_id = ?", openID).First(&user).Error; err == nil { - if attach.UserID != "" && user.ID != attach.UserID { - fmt.Printf("[PayNotify] 买家身份校验: attach.userId 与 openId 解析不一致,以 openId 为准\n") - } - buyerUserID = user.ID + defer resp.Body.Close() + for k, v := range resp.Header { + if len(v) > 0 { + c.Header(k, v[0]) } } - if buyerUserID == "" && attach.UserID != "" { - buyerUserID = attach.UserID - } - - // 更新订单状态 - var order model.Order - result := db.Where("order_sn = ?", orderSn).First(&order) - - if result.Error != nil { - // 订单不存在,补记订单 - fmt.Printf("[PayNotify] 订单不存在,补记订单: %s\n", orderSn) - - productID := attach.ProductID - if productID == "" { - productID = "fullbook" - } - productType := attach.ProductType - if productType == "" { - productType = "unknown" - } - desc := "支付回调补记订单" - status := "paid" - now := time.Now() - - order = model.Order{ - ID: orderSn, - OrderSN: orderSn, - UserID: buyerUserID, - OpenID: openID, - ProductType: productType, - ProductID: &productID, - Amount: totalAmount, - Description: &desc, - Status: &status, - TransactionID: &transactionID, - PayTime: &now, - } - - db.Create(&order) - } else if *order.Status != "paid" { - // 更新订单状态 - status := "paid" - now := time.Now() - db.Model(&order).Updates(map[string]interface{}{ - "status": status, - "transaction_id": transactionID, - "pay_time": now, - }) - fmt.Printf("[PayNotify] 订单状态已更新为已支付: %s\n", orderSn) - } else { - fmt.Printf("[PayNotify] 订单已支付,跳过更新: %s\n", orderSn) - } - - // 更新用户购买记录 - if buyerUserID != "" && attach.ProductType != "" { - if attach.ProductType == "fullbook" { - // 全书购买 - db.Model(&model.User{}).Where("id = ?", buyerUserID).Update("has_full_book", true) - fmt.Printf("[PayNotify] 用户已购全书: %s\n", buyerUserID) - } else if attach.ProductType == "section" && attach.ProductID != "" { - // 检查是否已有该章节的其他已支付订单 - var count int64 - db.Model(&model.Order{}).Where( - "user_id = ? AND product_type = 'section' AND product_id = ? AND status = 'paid' AND order_sn != ?", - buyerUserID, attach.ProductID, orderSn, - ).Count(&count) - - if count == 0 { - // 首次购买该章节,这里不需要更新 purchased_sections,因为查询时会从 orders 表读取 - fmt.Printf("[PayNotify] 用户首次购买章节: %s - %s\n", buyerUserID, attach.ProductID) - } else { - fmt.Printf("[PayNotify] 用户已有该章节的其他已支付订单: %s - %s\n", buyerUserID, attach.ProductID) - } - } - - // 清理相同产品的无效订单 - productID := attach.ProductID - if productID == "" { - productID = "fullbook" - } - - result := db.Where( - "user_id = ? AND product_type = ? AND product_id = ? AND status = 'created' AND order_sn != ?", - buyerUserID, attach.ProductType, productID, orderSn, - ).Delete(&model.Order{}) - - if result.RowsAffected > 0 { - fmt.Printf("[PayNotify] 已清理无效订单: %d 个\n", result.RowsAffected) - } - - // 处理分销佣金 - processReferralCommission(db, buyerUserID, totalAmount, orderSn) - } - - c.String(http.StatusOK, successResponse()) + c.Status(resp.StatusCode) + io.Copy(c.Writer, resp.Body) } // 处理分销佣金 diff --git a/soul-api/internal/handler/withdraw.go b/soul-api/internal/handler/withdraw.go index 8f727a07..a0b3e5c6 100644 --- a/soul-api/internal/handler/withdraw.go +++ b/soul-api/internal/handler/withdraw.go @@ -152,14 +152,57 @@ func WithdrawRecords(c *gin.Context) { if w.Status != nil { st = *w.Status } + canReceive := st == "processing" || st == "pending_confirm" out = append(out, gin.H{ "id": w.ID, "amount": w.Amount, "status": st, "createdAt": w.CreatedAt, "processedAt": w.ProcessedAt, + "canReceive": canReceive, }) } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": out}}) } +// WithdrawConfirmInfo GET /api/miniprogram/withdraw/confirm-info?id= 获取某条提现的领取零钱参数(mchId/appId/package),供 wx.requestMerchantTransfer 使用 +func WithdrawConfirmInfo(c *gin.Context) { + id := c.Query("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 id"}) + return + } + db := database.DB() + var w model.Withdrawal + if err := db.Where("id = ?", id).First(&w).Error; err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "提现记录不存在"}) + return + } + st := "" + if w.Status != nil { + st = *w.Status + } + if st != "processing" && st != "pending_confirm" { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "当前状态不可领取"}) + return + } + mchId := os.Getenv("WECHAT_MCH_ID") + if mchId == "" { + mchId = "1318592501" + } + appId := os.Getenv("WECHAT_APPID") + if appId == "" { + appId = "wxb8bbb2b10dec74aa" + } + // package 需由「用户确认模式」转账接口返回并落库,当前批量转账无 package,返回空;有值时可调 wx.requestMerchantTransfer + packageInfo := "" + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "mchId": mchId, + "appId": appId, + "package": packageInfo, + }, + }) +} + // WithdrawPendingConfirm GET /api/withdraw/pending-confirm?userId= 待确认/处理中收款列表 // 返回 pending、processing、pending_confirm 的提现,供小程序展示;并返回 mchId、appId 供确认收款用 func WithdrawPendingConfirm(c *gin.Context) { diff --git a/soul-api/internal/router/router.go b/soul-api/internal/router/router.go index 8ae26604..bf0ad960 100644 --- a/soul-api/internal/router/router.go +++ b/soul-api/internal/router/router.go @@ -240,6 +240,7 @@ func Setup(cfg *config.Config) *gin.Engine { miniprogram.POST("/withdraw", handler.WithdrawPost) miniprogram.GET("/withdraw/records", handler.WithdrawRecords) miniprogram.GET("/withdraw/pending-confirm", handler.WithdrawPendingConfirm) + miniprogram.GET("/withdraw/confirm-info", handler.WithdrawConfirmInfo) } // ----- 提现 ----- diff --git a/soul-api/internal/wechat/miniprogram.go b/soul-api/internal/wechat/miniprogram.go index d27c13b2..1a721b83 100644 --- a/soul-api/internal/wechat/miniprogram.go +++ b/soul-api/internal/wechat/miniprogram.go @@ -3,18 +3,23 @@ package wechat import ( "bytes" "context" - "crypto/md5" "encoding/json" "fmt" "io" "net/http" - "strconv" + "os" + "path/filepath" + "strings" "time" "soul-api/internal/config" + "github.com/ArtisanCloud/PowerLibs/v3/object" "github.com/ArtisanCloud/PowerWeChat/v3/src/miniProgram" + "github.com/ArtisanCloud/PowerWeChat/v3/src/kernel/models" "github.com/ArtisanCloud/PowerWeChat/v3/src/payment" + notifyrequest "github.com/ArtisanCloud/PowerWeChat/v3/src/payment/notify/request" + "github.com/ArtisanCloud/PowerWeChat/v3/src/payment/order/request" ) var ( @@ -23,30 +28,86 @@ var ( cfg *config.Config ) -// Init 初始化微信客户端 +// resolveCertPaths 若证书/私钥路径为 URL 则下载到临时文件并返回本地路径 +func resolveCertPaths(c *config.Config) (certPath, keyPath string, err error) { + certPath = c.WechatCertPath + keyPath = c.WechatKeyPath + if certPath == "" || keyPath == "" { + return certPath, keyPath, nil + } + if strings.HasPrefix(keyPath, "http://") || strings.HasPrefix(keyPath, "https://") { + dir, e := os.MkdirTemp("", "wechat_cert_*") + if e != nil { + return "", "", fmt.Errorf("创建临时目录失败: %w", e) + } + keyPath, e = downloadToFile(keyPath, filepath.Join(dir, "apiclient_key.pem")) + if e != nil { + return "", "", e + } + if strings.HasPrefix(certPath, "http://") || strings.HasPrefix(certPath, "https://") { + certPath, e = downloadToFile(certPath, filepath.Join(dir, "apiclient_cert.pem")) + if e != nil { + return "", "", e + } + } else { + // cert 是本地路径,只下载了 key + certPath = c.WechatCertPath + } + } + return certPath, keyPath, nil +} + +func downloadToFile(url, filePath string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("下载文件失败: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("下载返回状态: %d", resp.StatusCode) + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取内容失败: %w", err) + } + if err := os.WriteFile(filePath, data, 0600); err != nil { + return "", fmt.Errorf("写入临时文件失败: %w", err) + } + return filePath, nil +} + +// Init 初始化微信客户端(小程序 + 支付 v3 + 转账均使用 PowerWeChat) func Init(c *config.Config) error { cfg = c - - // 初始化小程序 + var err error miniProgramApp, err = miniProgram.NewMiniProgram(&miniProgram.UserConfig{ - AppID: cfg.WechatAppID, - Secret: cfg.WechatAppSecret, + AppID: cfg.WechatAppID, + Secret: cfg.WechatAppSecret, HttpDebug: cfg.Mode == "debug", }) if err != nil { return fmt.Errorf("初始化小程序失败: %w", err) } - // 初始化支付(v2) - paymentApp, err = payment.NewPayment(&payment.UserConfig{ - AppID: cfg.WechatAppID, - MchID: cfg.WechatMchID, - Key: cfg.WechatMchKey, - HttpDebug: cfg.Mode == "debug", - }) + certPath, keyPath, err := resolveCertPaths(cfg) if err != nil { - return fmt.Errorf("初始化支付失败: %w", err) + return fmt.Errorf("解析证书路径: %w", err) + } + paymentConfig := &payment.UserConfig{ + AppID: cfg.WechatAppID, + MchID: cfg.WechatMchID, + MchApiV3Key: cfg.WechatAPIv3Key, + Key: cfg.WechatMchKey, + CertPath: certPath, + KeyPath: keyPath, + SerialNo: cfg.WechatSerialNo, + NotifyURL: cfg.WechatNotifyURL, + HttpDebug: cfg.Mode == "debug", + } + paymentApp, err = payment.NewPayment(paymentConfig) + if err != nil { + return fmt.Errorf("初始化支付(v3)失败: %w", err) } return nil @@ -188,199 +249,119 @@ func GenerateMiniProgramCode(scene, page string, width int) ([]byte, error) { return body, nil } -// PayV2UnifiedOrder 微信支付 v2 统一下单 -func PayV2UnifiedOrder(params map[string]string) (map[string]string, error) { - // 添加必要参数 - params["appid"] = cfg.WechatAppID - params["mch_id"] = cfg.WechatMchID - params["nonce_str"] = generateNonceStr() - params["sign_type"] = "MD5" - - // 生成签名 - params["sign"] = generateSign(params, cfg.WechatMchKey) - - // 转换为 XML - xmlData := mapToXML(params) - - // 发送请求 - resp, err := http.Post("https://api.mch.weixin.qq.com/pay/unifiedorder", "application/xml", bytes.NewReader([]byte(xmlData))) +// GetPayNotifyURL 返回支付回调地址(与商户平台配置一致) +func GetPayNotifyURL() string { + if cfg != nil && cfg.WechatNotifyURL != "" { + return cfg.WechatNotifyURL + } + return "https://soul.quwanzhi.com/api/miniprogram/pay/notify" +} + +// PayJSAPIOrder 微信支付 v3 小程序 JSAPI 统一下单,返回 prepay_id +func PayJSAPIOrder(ctx context.Context, openID, orderSn string, amountCents int, description, attach string) (prepayID string, err error) { + if paymentApp == nil { + return "", fmt.Errorf("支付未初始化") + } + req := &request.RequestJSAPIPrepay{ + PrepayBase: request.PrepayBase{ + AppID: cfg.WechatAppID, + MchID: cfg.WechatMchID, + NotifyUrl: GetPayNotifyURL(), + }, + Description: description, + OutTradeNo: orderSn, + Amount: &request.JSAPIAmount{ + Total: amountCents, + Currency: "CNY", + }, + Payer: &request.JSAPIPayer{OpenID: openID}, + Attach: attach, + } + res, err := paymentApp.Order.JSAPITransaction(ctx, req) if err != nil { - return nil, fmt.Errorf("请求统一下单接口失败: %w", err) + return "", err } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - result := xmlToMap(string(body)) - - if result["return_code"] != "SUCCESS" { - return nil, fmt.Errorf("统一下单失败: %s", result["return_msg"]) + if res == nil || res.PrepayID == "" { + return "", fmt.Errorf("微信返回 prepay_id 为空") } - - if result["result_code"] != "SUCCESS" { - return nil, fmt.Errorf("下单失败: %s", result["err_code_des"]) - } - - return result, nil + return res.PrepayID, nil } -// PayV2OrderQuery 微信支付 v2 订单查询 -func PayV2OrderQuery(outTradeNo string) (map[string]string, error) { - params := map[string]string{ - "appid": cfg.WechatAppID, - "mch_id": cfg.WechatMchID, - "out_trade_no": outTradeNo, - "nonce_str": generateNonceStr(), +// GetJSAPIPayParams 根据 prepay_id 生成小程序 wx.requestPayment 所需参数(v3 签名) +func GetJSAPIPayParams(prepayID string) (map[string]string, error) { + if paymentApp == nil { + return nil, fmt.Errorf("支付未初始化") } - - params["sign"] = generateSign(params, cfg.WechatMchKey) - xmlData := mapToXML(params) - - resp, err := http.Post("https://api.mch.weixin.qq.com/pay/orderquery", "application/xml", bytes.NewReader([]byte(xmlData))) + cfgMap, err := paymentApp.JSSDK.BridgeConfig(prepayID, false) if err != nil { - return nil, fmt.Errorf("请求订单查询接口失败: %w", err) + return nil, err } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - result := xmlToMap(string(body)) - - return result, nil -} - -// VerifyPayNotify 验证支付回调签名 -func VerifyPayNotify(data map[string]string) bool { - receivedSign := data["sign"] - if receivedSign == "" { - return false - } - - delete(data, "sign") - calculatedSign := generateSign(data, cfg.WechatMchKey) - - return receivedSign == calculatedSign -} - -// GenerateJSAPIPayParams 生成小程序支付参数 -func GenerateJSAPIPayParams(prepayID string) map[string]string { - timestamp := strconv.FormatInt(time.Now().Unix(), 10) - nonceStr := generateNonceStr() - - params := map[string]string{ - "appId": cfg.WechatAppID, - "timeStamp": timestamp, - "nonceStr": nonceStr, - "package": fmt.Sprintf("prepay_id=%s", prepayID), - "signType": "MD5", - } - - params["paySign"] = generateSign(params, cfg.WechatMchKey) - - return params -} - -// === 辅助函数 === - -func generateNonceStr() string { - return fmt.Sprintf("%d", time.Now().UnixNano()) -} - -func generateSign(params map[string]string, key string) string { - // 按字典序排序 - var keys []string - for k := range params { - if k != "sign" && params[k] != "" { - keys = append(keys, k) + out := make(map[string]string) + if m, ok := cfgMap.(*object.StringMap); ok && m != nil { + for k, v := range *m { + out[k] = v } } - - // 简单冒泡排序 - for i := 0; i < len(keys); i++ { - for j := i + 1; j < len(keys); j++ { - if keys[i] > keys[j] { - keys[i], keys[j] = keys[j], keys[i] - } - } - } - - // 拼接字符串 - var signStr string - for _, k := range keys { - signStr += fmt.Sprintf("%s=%s&", k, params[k]) - } - signStr += fmt.Sprintf("key=%s", key) - - // MD5 - hash := md5.Sum([]byte(signStr)) - return fmt.Sprintf("%X", hash) // 大写 -} - -func mapToXML(data map[string]string) string { - xml := "" - for k, v := range data { - xml += fmt.Sprintf("<%s>", k, v, k) - } - xml += "" - return xml -} - -func xmlToMap(xmlStr string) map[string]string { - result := make(map[string]string) - - // 简单的 XML 解析(仅支持 value 格式) - var key, value string - inCDATA := false - inTag := false - isClosing := false - - for i := 0; i < len(xmlStr); i++ { - ch := xmlStr[i] - - if ch == '<' { - if i+1 < len(xmlStr) && xmlStr[i+1] == '/' { - isClosing = true - i++ // skip '/' - } else if i+8 < len(xmlStr) && xmlStr[i:i+9] == "' { - inTag = false - if isClosing { - if key != "" && key != "xml" { - result[key] = value + if len(out) == 0 && cfgMap != nil { + if ms, ok := cfgMap.(map[string]interface{}); ok { + for k, v := range ms { + if s, ok := v.(string); ok { + out[k] = s } - key = "" - value = "" - isClosing = false } - continue - } - - if inCDATA && i+2 < len(xmlStr) && xmlStr[i:i+3] == "]]>" { - inCDATA = false - i += 2 - continue - } - - if inTag { - key += string(ch) - } else if !isClosing { - value += string(ch) } } - - return result + return out, nil } -// XMLToMap 导出供外部使用 -func XMLToMap(xmlStr string) map[string]string { - return xmlToMap(xmlStr) +// QueryOrderByOutTradeNo 根据商户订单号查询订单状态(v3) +func QueryOrderByOutTradeNo(ctx context.Context, outTradeNo string) (tradeState, transactionID string, totalFee int, err error) { + if paymentApp == nil { + return "", "", 0, fmt.Errorf("支付未初始化") + } + res, err := paymentApp.Order.QueryByOutTradeNumber(ctx, outTradeNo) + if err != nil { + return "", "", 0, err + } + if res == nil { + return "", "", 0, nil + } + tradeState = res.TradeState + transactionID = res.TransactionID + if res.Amount != nil { + totalFee = int(res.Amount.Total) + } + return tradeState, transactionID, totalFee, nil +} + +// HandlePayNotify 处理 v3 支付回调:验签并解密后调用 handler,返回应写回微信的 HTTP 响应 +// handler 参数:orderSn, transactionID, totalFee(分), attach(JSON), openID +func HandlePayNotify(req *http.Request, handler func(orderSn, transactionID string, totalFee int, attach, openID string) error) (*http.Response, error) { + if paymentApp == nil { + return nil, fmt.Errorf("支付未初始化") + } + return paymentApp.HandlePaidNotify(req, func(_ *notifyrequest.RequestNotify, transaction *models.Transaction, fail func(string)) interface{} { + if transaction == nil { + fail("transaction is nil") + return nil + } + orderSn := transaction.OutTradeNo + transactionID := transaction.TransactionID + totalFee := 0 + if transaction.Amount != nil { + totalFee = int(transaction.Amount.Total) + } + attach := transaction.Attach + openID := "" + if transaction.Payer != nil { + openID = transaction.Payer.OpenID + } + if err := handler(orderSn, transactionID, totalFee, attach, openID); err != nil { + fail(err.Error()) + return nil + } + return nil + }) } // GenerateOrderSn 生成订单号 diff --git a/soul-api/internal/wechat/transfer.go b/soul-api/internal/wechat/transfer.go index 107565a8..d7c6fc8d 100644 --- a/soul-api/internal/wechat/transfer.go +++ b/soul-api/internal/wechat/transfer.go @@ -2,108 +2,25 @@ package wechat import ( "context" - "crypto/rsa" - "crypto/x509" - "encoding/pem" "fmt" - "io" - "net/http" - "os" - "strings" "time" "soul-api/internal/config" - "github.com/wechatpay-apiv3/wechatpay-go/core" - "github.com/wechatpay-apiv3/wechatpay-go/core/option" - "github.com/wechatpay-apiv3/wechatpay-go/services/transferbatch" + "github.com/ArtisanCloud/PowerLibs/v3/object" + "github.com/ArtisanCloud/PowerWeChat/v3/src/payment/transfer/request" ) -var ( - transferClient *core.Client - transferCfg *config.Config -) - -// InitTransfer 初始化转账客户端 -func InitTransfer(c *config.Config) error { - transferCfg = c - - // 加载商户私钥 - privateKey, err := loadPrivateKey(c.WechatKeyPath) - if err != nil { - return fmt.Errorf("加载商户私钥失败: %w", err) - } - - // 初始化客户端 - opts := []core.ClientOption{ - option.WithWechatPayAutoAuthCipher(c.WechatMchID, c.WechatSerialNo, privateKey, c.WechatAPIv3Key), - } - - client, err := core.NewClient(context.Background(), opts...) - if err != nil { - return fmt.Errorf("初始化微信支付客户端失败: %w", err) - } - - transferClient = client - return nil -} - -// loadPrivateKey 加载商户私钥。path 支持本地路径或 http(s) 链接(如 OSS 地址)。 -func loadPrivateKey(path string) (*rsa.PrivateKey, error) { - var privateKeyBytes []byte - if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { - resp, err := http.Get(path) - if err != nil { - return nil, fmt.Errorf("从链接获取私钥失败: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("获取私钥返回异常状态: %d", resp.StatusCode) - } - privateKeyBytes, err = io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("读取私钥内容失败: %w", err) - } - } else { - var err error - privateKeyBytes, err = os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("读取私钥文件失败: %w", err) - } - } - - block, _ := pem.Decode(privateKeyBytes) - if block == nil { - return nil, fmt.Errorf("解析私钥失败:无效的 PEM 格式") - } - - privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - // 尝试 PKCS8 格式 - key, err2 := x509.ParsePKCS8PrivateKey(block.Bytes) - if err2 != nil { - return nil, fmt.Errorf("解析私钥失败: %w", err) - } - var ok bool - privateKey, ok = key.(*rsa.PrivateKey) - if !ok { - return nil, fmt.Errorf("私钥不是 RSA 格式") - } - } - - return privateKey, nil -} - // TransferParams 转账参数 type TransferParams struct { - OutBatchNo string // 商家批次单号(唯一) - OutDetailNo string // 商家明细单号(唯一) - OpenID string // 收款用户 openid - Amount int // 转账金额(分) - UserName string // 收款用户姓名(可选,用于实名校验) - Remark string // 转账备注 - BatchName string // 批次名称(如"提现") - BatchRemark string // 批次备注 + OutBatchNo string // 商家批次单号(唯一) + OutDetailNo string // 商家明细单号(唯一) + OpenID string // 收款用户 openid + Amount int // 转账金额(分) + UserName string // 收款用户姓名(可选,用于实名校验) + Remark string // 转账备注 + BatchName string // 批次名称(如"提现") + BatchRemark string // 批次备注 } // TransferResult 转账结果 @@ -111,69 +28,96 @@ type TransferResult struct { BatchID string // 微信批次单号 OutBatchNo string // 商家批次单号 CreateTime time.Time // 批次创建时间 - BatchStatus string // 批次状态:ACCEPTED-已受理, PROCESSING-处理中, FINISHED-已完成, CLOSED-已关闭 + BatchStatus string // 批次状态:ACCEPTED-已受理 等 } -// InitiateTransfer 发起转账 +// InitTransfer 保留兼容:转账已由 Init() 中 PowerWeChat Payment 统一初始化,调用无副作用 +func InitTransfer(_ *config.Config) error { + return nil +} + +// InitiateTransfer 发起商家转账到零钱(PowerWeChat TransferBatch) func InitiateTransfer(params TransferParams) (*TransferResult, error) { - if transferClient == nil { - return nil, fmt.Errorf("转账客户端未初始化") + if paymentApp == nil { + return nil, fmt.Errorf("支付/转账未初始化,请先调用 wechat.Init") } - svc := transferbatch.TransferBatchApiService{Client: transferClient} - - // 构建转账明细 - details := []transferbatch.TransferDetailInput{ - { - OutDetailNo: core.String(params.OutDetailNo), - TransferAmount: core.Int64(int64(params.Amount)), - TransferRemark: core.String(params.Remark), - Openid: core.String(params.OpenID), - }, + detail := &request.TransferDetail{ + OutDetailNO: params.OutDetailNo, + TransferAmount: params.Amount, + TransferRemark: params.Remark, + OpenID: params.OpenID, } - - // 如果提供了姓名,添加实名校验 if params.UserName != "" { - details[0].UserName = core.String(params.UserName) + detail.UserName = object.NewNullString(params.UserName, true) + } + req := &request.RequestTransferBatch{ + AppID: cfg.WechatAppID, + OutBatchNO: params.OutBatchNo, + BatchName: params.BatchName, + BatchRemark: params.BatchRemark, + TotalAmount: params.Amount, + TotalNum: 1, + TransferDetailList: []*request.TransferDetail{detail}, + } + if cfg.WechatTransferURL != "" { + req.SetNotifyUrl(cfg.WechatTransferURL) } - // 发起转账请求 - req := transferbatch.InitiateBatchTransferRequest{ - Appid: core.String(transferCfg.WechatAppID), - OutBatchNo: core.String(params.OutBatchNo), - BatchName: core.String(params.BatchName), - BatchRemark: core.String(params.BatchRemark), - TotalAmount: core.Int64(int64(params.Amount)), - TotalNum: core.Int64(1), - TransferDetailList: details, - } - - resp, result, err := svc.InitiateBatchTransfer(context.Background(), req) + resp, err := paymentApp.TransferBatch.Batch(context.Background(), req) if err != nil { return nil, fmt.Errorf("发起转账失败: %w", err) } - - if result.Response.StatusCode != 200 { - return nil, fmt.Errorf("转账请求失败,状态码: %d", result.Response.StatusCode) + if resp == nil { + return nil, fmt.Errorf("转账返回为空") } - return &TransferResult{ - BatchID: *resp.BatchId, - OutBatchNo: *resp.OutBatchNo, - CreateTime: *resp.CreateTime, + result := &TransferResult{ + OutBatchNo: resp.OutBatchNo, BatchStatus: "ACCEPTED", - }, nil + } + if resp.BatchId != "" { + result.BatchID = resp.BatchId + } + if !resp.CreateTime.IsZero() { + result.CreateTime = resp.CreateTime + } + return result, nil } -// QueryTransfer 查询转账结果(暂不实现,转账状态通过回调获取) +// QueryTransfer 查询转账结果(可选,转账状态也可通过回调获取) func QueryTransfer(outBatchNo, outDetailNo string) (map[string]interface{}, error) { - // TODO: 实现查询转账结果 - // 微信转账采用异步模式,通过回调通知最终结果 + if paymentApp == nil { + return map[string]interface{}{ + "out_batch_no": outBatchNo, + "out_detail_no": outDetailNo, + "status": "unknown", + "message": "转账未初始化", + }, nil + } + detail, err := paymentApp.TransferBatch.QueryOutBatchNoDetail(context.Background(), outBatchNo, outDetailNo) + if err != nil { + return map[string]interface{}{ + "out_batch_no": outBatchNo, + "out_detail_no": outDetailNo, + "status": "processing", + "message": err.Error(), + }, nil + } + if detail == nil { + return map[string]interface{}{ + "out_batch_no": outBatchNo, + "out_detail_no": outDetailNo, + "status": "processing", + "message": "转账处理中", + }, nil + } return map[string]interface{}{ - "out_batch_no": outBatchNo, - "out_detail_no": outDetailNo, - "status": "processing", - "message": "转账处理中,请等待回调通知", + "out_batch_no": outBatchNo, + "out_detail_no": outDetailNo, + "detail_status": detail.DetailStatus, + "fail_reason": detail.FailReason, + "transfer_amount": detail.TransferAmount, }, nil } @@ -192,32 +136,3 @@ func GenerateTransferDetailNo() string { random := now.UnixNano() % 1000000 return fmt.Sprintf("WDD%s%06d", timestamp, random) } - -// TransferNotifyResult 转账回调结果 -type TransferNotifyResult struct { - MchID *string `json:"mchid"` - OutBatchNo *string `json:"out_batch_no"` - BatchID *string `json:"batch_id"` - AppID *string `json:"appid"` - OutDetailNo *string `json:"out_detail_no"` - DetailID *string `json:"detail_id"` - DetailStatus *string `json:"detail_status"` - TransferAmount *int64 `json:"transfer_amount"` - OpenID *string `json:"openid"` - UserName *string `json:"user_name"` - InitiateTime *string `json:"initiate_time"` - UpdateTime *string `json:"update_time"` - FailReason *string `json:"fail_reason"` -} - -// VerifyTransferNotify 验证转账回调签名(使用 notify handler) -func VerifyTransferNotify(ctx context.Context, request interface{}) (*TransferNotifyResult, error) { - // 微信官方 SDK 的回调处理 - // 实际使用时,微信会 POST JSON 数据,包含加密信息 - // 这里暂时返回简化版本,实际项目中需要完整实现签名验证 - - // TODO: 完整实现回调验证 - // 需要解析请求体中的 resource.ciphertext,使用 APIv3 密钥解密 - - return &TransferNotifyResult{}, fmt.Errorf("转账回调需要完整实现") -} diff --git a/soul-api/soul-api b/soul-api/soul-api index cfa2e531..fc73652b 100644 Binary files a/soul-api/soul-api and b/soul-api/soul-api differ diff --git a/soul-api/tmp/build-errors.log b/soul-api/tmp/build-errors.log deleted file mode 100644 index 40b5ed1e..00000000 --- a/soul-api/tmp/build-errors.log +++ /dev/null @@ -1 +0,0 @@ -exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/soul-api/tmp/main.exe b/soul-api/tmp/main.exe deleted file mode 100644 index a6b60fe6..00000000 Binary files a/soul-api/tmp/main.exe and /dev/null differ diff --git a/soul-api/wechat/info.log b/soul-api/wechat/info.log index 48e941af..8454e530 100644 --- a/soul-api/wechat/info.log +++ b/soul-api/wechat/info.log @@ -6,3 +6,17 @@ {"level":"debug","timestamp":"2026-02-09T17:09:56+08:00","caller":"kernel/accessToken.go:383","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 174\r\nConnection: keep-alive\r\nContent-Type: application/json; encoding=utf-8\r\nDate: Mon, 09 Feb 2026 09:09:56 GMT\r\n\r\n{\"access_token\":\"100_pYtS9qgA2TynfkbEwQA-PVIvZ86ZH242clHJGzB3xBUzePhWpBtlPpbpF1WPa-ksZ0X1pCgQ3GZdOQx7hI8LTBZBzspO5Y7HD2__FmVGTBHEEXBO5KC8LtHtGCgWHLjAIAEPT\",\"expires_in\":7200}"} {"level":"debug","timestamp":"2026-02-09T17:09:56+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=100_pYtS9qgA2TynfkbEwQA-PVIvZ86ZH242clHJGzB3xBUzePhWpBtlPpbpF1WPa-ksZ0X1pCgQ3GZdOQx7hI8LTBZBzspO5Y7HD2__FmVGTBHEEXBO5KC8LtHtGCgWHLjAIAEPT&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0e3misGa1mI8bL07SQGa1ooPA01misGI&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "} {"level":"debug","timestamp":"2026-02-09T17:09:56+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Mon, 09 Feb 2026 09:09:56 GMT\r\n\r\n{\"session_key\":\"GPZHc5k1ud3stnTc4LX9LA==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"} +{"level":"debug","timestamp":"2026-02-09T21:01:40+08:00","caller":"kernel/accessToken.go:381","content":"GET https://api.weixin.qq.com/cgi-bin/token?appid=wxb8bbb2b10dec74aa&grant_type=client_credential&neededText=&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "} +{"level":"debug","timestamp":"2026-02-09T21:01:40+08:00","caller":"kernel/accessToken.go:383","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 174\r\nConnection: keep-alive\r\nContent-Type: application/json; encoding=utf-8\r\nDate: Mon, 09 Feb 2026 13:01:40 GMT\r\n\r\n{\"access_token\":\"100_JhZvnxAjmhcUfwICXoOlWusynL85niD6i5OluMR0j7l3Xvcxav5f-4RqqSNr0z9znW-hhb8gT500JyEqepFbmx8CETxPKnx5g7Ed7ixtU0bQ4-e6CBFLvNvJbBYENSdADAXGL\",\"expires_in\":7200}"} +{"level":"debug","timestamp":"2026-02-09T21:01:41+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=100_JhZvnxAjmhcUfwICXoOlWusynL85niD6i5OluMR0j7l3Xvcxav5f-4RqqSNr0z9znW-hhb8gT500JyEqepFbmx8CETxPKnx5g7Ed7ixtU0bQ4-e6CBFLvNvJbBYENSdADAXGL&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0d3Ozall2mjA9h4rEmnl22ECCk0OzalV&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "} +{"level":"debug","timestamp":"2026-02-09T21:01:41+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Mon, 09 Feb 2026 13:01:41 GMT\r\n\r\n{\"session_key\":\"OqKwsfM50RFlBS8CQw0+yg==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"} +{"level":"debug","timestamp":"2026-02-09T21:02:11+08:00","caller":"kernel/baseClient.go:457","content":"POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"aGO4P5HHXBrt6CNonfKpVB4WV2u21YVU\",timestamp=\"1770642130\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"febgksEBEJOEgVPLjNIdHo/RbWIJOnNuxQZwj/dzHsqtEbLcJ7yS/Wwqt5b3OaM3LHT2EvBBRl9VeFKYl63OvgDsaQ/B7GBAQSYFmd5CR4eEt1YjaoWqF6OR3vzzkX69R3kru2Mg2qxfvRPq3EtMkX0ag3wvERT48rmCHuCCCj/LduIrimxaCHyauPTZ/MijhDEIZqY+nr8Xh4h9ptexXrmckmUh8VqAO6WwVOcOT2fPatiZHm2aYf/COCrcUGBSzYf1nLFiOgoAqGVx15+A/9MgjIGl5B3OTR0jcaYHhEJjlczfXeewRapy4W7k5XVz5BSCu6XFfaMG+PFDehZohw==\"Accept:*/*} request body:"} +{"level":"debug","timestamp":"2026-02-09T21:02:11+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 52\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Mon, 09 Feb 2026 13:02:11 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08D3B5A7CC06104518DACEC6AF0120F6881428FF8D01-0\r\nServer: nginx\r\nWechatpay-Nonce: be0fd11f017d9ab62db0e1e4b7ba9263\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: fa0/KwmLBVTlAeLLeomVFOfj6uK0FDpZ//CHG5/xZuEKvfHnTetsaAMaZYqwaaYnGIjl1k6rh33g++vrBDV2NROn8TZIkf1LD+jHKSyZNxw2VlPrKzyS6/Lfg/vCRxXULw1MteL+IK/6IJmRwmm5/n9ylWe8VpQD/FnjuKskAfrntMU9CUvhAkdE3g+a2p7YhvVJipzcHVaAaiu4Acs4wDekTklBAGVxNKeoIK+kyq6AyvQqV1byOAHPvSFjKHoooQiCtZ/OgBy/HF5fcjtlhQvDpTXl0w6InIOdW9Yaa0DYTF1sEB8ryZLU+FtkSdFS79ZYnW88wzhae9YM4jZUuA==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1770642131\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"prepay_id\":\"wx092102112493393686135f0e8978740000\"}"} +{"level":"debug","timestamp":"2026-02-09T21:02:16+08:00","caller":"kernel/baseClient.go:457","content":"POST https://api.mch.weixin.qq.com/v3/transfer/batches request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"FadMCHICw93ljFjPi5HUYycBhCg5ksoH\",timestamp=\"1770642136\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"wr8vJWcLdSfHqgrpgTI+7Fnsvh6KVEFx6n4VpR2u1yrrQVOU+HIfyPqkYVbwawCX2pSpsgV4AVfPNzeRhPcrj4e+aOaibKx3QQrU9sl4L+N2Hi/y1Z4qtXpZP2R1NHpCFZcpkmZ3L6GME/PvZDcVkNE9ygm2veoGJo86Al+Yv3dYVXleV3nt0livG1HsjVNBA5sf6bTz3OSyXorNTJhrCDuxrS4n0dEQ8FJfkuBGMO477Z5eVaGDJ0vsXF+2Abg5GjpuF5IN1aW45VSeCazRIn8b5/wozuplnfLIrDcacIrkYaTlai7DLrdV6vAtj0imPWkhOOwLdhakZBcwWVaZlw==\"Accept:*/*} request body:"} +{"level":"debug","timestamp":"2026-02-09T21:02:16+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 400 Bad Request\r\nContent-Length: 207\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Mon, 09 Feb 2026 13:02:16 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08D8B5A7CC0610F9021896D4BCA80120BA0928B8CC04-268435462\r\nServer: nginx\r\nWechatpay-Nonce: 4515f64471c3a5fd2b341c2049064a3b\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: lS6QkNFXP/Yp6wQ/t4NulZLk+tWPMi7v11i/FH4gGqBvGHL6q6u1x8e7jUHrbmdTHFhsAMcbC3uWGRE5e+2F8mJWso9ufxVoAgcgWVEmJ8q6HTWo1JYJdQFbx3XCvU1rcLgZ/+LwSwhlDc2gFptMiDh3g8cLFhhFVmg4hjVS/seXyH3dlQRMDdinlyEF1U4C29zD++6ZfqUf0LDwFpsgjxflA/EHAFMJClsnhYDattgLp814wfqvEqTxjQYpDH6mDyBpvr2jWaBQpyiXsgVgsycF5ZZg4OonWoOgrUt9PajK23uRbnhf2zIs+vE4Xj7pJut8q1zPlehf7MWXOhsMTw==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1770642136\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"code\":\"PARAM_ERROR\",\"detail\":{\"location\":\"body\",\"value\":41},\"message\":\"输入源“/body/batch_remark”映射到值字段“批次备注”字符串规则校验失败,字符数 41,大于最大值 32\"}"} +{"level":"debug","timestamp":"2026-02-09T21:06:59+08:00","caller":"kernel/baseClient.go:457","content":"POST https://api.mch.weixin.qq.com/v3/transfer/batches request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"sZNAinJsyjZxOnM09g8lPSS8evISRl8C\",timestamp=\"1770642419\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"nIfJBwuXQ3FgSQ9yP++wklvZazLTi+Ry8SZySNkS64NjUe8CtGkg1oE/xi5GQtqfzVn0h7uAhjbX9N0vuDj4rIh51izVmFLiGipSoFZsF71P3Xy2uFaRd3Dk8ECAyWHi1oheE7B9YYaUj7El3B0EKjgrz9TVViYdtlTrh1HtEs/yFk1YU0uV8O13Ew4WEFoDml7liBG7v8/g73aekOuCrzzBzGk913awI3xUptcd2/LzXjQ2/B+kEXTB4WaPa7IBu687Df/ZiuiMFl1SzIY3dGPObzIJpWsV916WLJGyBcj+89S2wj7XjDyJ3AUk6K15ySfy1ryEEYvU0I4Vc9fnYg==\"Accept:*/*} request body:"} +{"level":"debug","timestamp":"2026-02-09T21:06:59+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 400 Bad Request\r\nContent-Length: 207\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Mon, 09 Feb 2026 13:06:59 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08F3B7A7CC06108B0618B3D7BCA80120831328921A-268435462\r\nServer: nginx\r\nWechatpay-Nonce: b2b8f4ff0c0c9cb82f3df79d1edc8441\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: mRqSwUzooJKTuzV6KNXK5Ufjl5XlRFpypBeMC67N5FDxU5Zzh4ENL4aoVJol3pn02Xn/cSJpicvWz7jHTHqwoMuRBbkFgg0QQk/2FQk8ay90SATEO5opSuIj2BHRS/e2rvpia1CeHNMnemBOKjTPtao9klJE50QPDUok+SLLNJjs72YkMVI3l+/Yq2BV4JGbpUHb4GBuEVN4+Ug+lvuOayOhuuhHZyD3wajxsgjBhm3Qylpg8q66r4GLFrRhaotO5OTgNPULLKIcQBHxjE2aDQJk+8+U1cje1QxMCnT9LXeb/0/tsjEMFwUJ2A5OEbGxuyfgGI8HPUspAEOvipYlqg==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1770642419\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"code\":\"PARAM_ERROR\",\"detail\":{\"location\":\"body\",\"value\":41},\"message\":\"输入源“/body/batch_remark”映射到值字段“批次备注”字符串规则校验失败,字符数 41,大于最大值 32\"}"} +{"level":"debug","timestamp":"2026-02-09T21:28:10+08:00","caller":"kernel/baseClient.go:457","content":"POST https://api.mch.weixin.qq.com/v3/transfer/batches request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"6KEBYwbox0dA0vxNd8CDWHsr6avVDSBT\",timestamp=\"1770643689\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"cTBb/Kct+l74ICF5uwF27nV6AXk/iQ5Wtvt3p8sTat14Ou5zUSDAkjLWS8ORQFAVzgLd+mKhY7QwpM2QxovtautyvSYOwubnvf/JWuAu6lywyExJOZZRz5eSfjfE0y9psdGGmwmrSmI+f/DgxtDSAEumlwGnFUddNXIbke/mDyrJQ3cnZMUSkq0yd9X367+VmOrHIRtP9nzGBvawNY4EZ1I9DbTi0IOpwgwodoqs5XFSXVayXrJaYauKy0TBE4y4KXgaTYJ3q0CGPBmjGlqi75lA4AAOhJ1Eo+OPRsfhkmRlqw86bEjTBwmpagtIzMkoD9FW6pdUjYLQLTYEo3mZ3A==\"Accept:*/*} request body:"} +{"level":"debug","timestamp":"2026-02-09T21:28:10+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 400 Bad Request\r\nContent-Length: 207\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Mon, 09 Feb 2026 13:28:10 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08EAC1A7CC0610EF0118B3D7BCA80120AD2028BF3A-268435462\r\nServer: nginx\r\nWechatpay-Nonce: a16fd3886807033516450570c6c04a12\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: S0I6R3gPOBPRTDXda0qgP7+htE2eBWPa6ypGyF/dY9CMEs/FAi2sTa9rSGovCTpOT2veC2w6UJZqHOVJ7RYNxyMCHei/mcGg4oZsUcr1qhDBqRkF4GtKQ5VAtxykdyvqoHNEwBG6ZVXaOXvDlIhVer+h8p/ETdmts+m3DSihLZ0ERQakUgilzMkvpCEWtZ9Z0o7zdIjB6dY2fJksQNEoDTsBU4AweU3O+6PIhReJD58iNrJoPdoUmnTedvIUyz4kaCPZo+QKD8k+VdzzuOa7SC1VrDvcnPdWAuEVUtjuZW4lN8VUCD0/I/N2WwYZzxc37Ai081U4u09GZA0TgTcjSQ==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1770643690\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"code\":\"PARAM_ERROR\",\"detail\":{\"location\":\"body\",\"value\":41},\"message\":\"输入源“/body/batch_remark”映射到值字段“批次备注”字符串规则校验失败,字符数 41,大于最大值 32\"}"} +{"level":"debug","timestamp":"2026-02-09T21:28:32+08:00","caller":"kernel/baseClient.go:457","content":"POST https://api.mch.weixin.qq.com/v3/transfer/batches request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"gC08Q1aUBPsVnMhCKLKj77AiQIgqxeQk\",timestamp=\"1770643712\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"nMG2gpkUbjGyR/Mp45By3DUxCCL3gaPmhS0rTD3xMhxttP0Bd5n1/urSMMELmfi/y3zU3zlM2mgh4hqrbDWhQ9SQBq6una1b1C+w1S0It2G6pXGdHggpigc7sfjm7kABZaEPcejHiq1+HnKJ7MiCLRP3uf3UKXT4/brcUtVE2TjoBP9+nyMP93FOnqWlsc+FX4nv3RrsBwoIzOYWfSzwZ87DJ+fOAtd2pJiwOEqUPSnbUBaONbHj+aY3xAqTNkzfoMVyFvar6o9L9UFS0CEgjbo3bzVtKW/vT9SSFt9ZLlvSaexadYlrm2jKRSSO6N5KJRxPbgu1HJGcgxKafo5Bgw==\"Accept:*/*} request body:"} +{"level":"debug","timestamp":"2026-02-09T21:28:32+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 400 Bad Request\r\nContent-Length: 207\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Mon, 09 Feb 2026 13:28:32 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 0880C2A7CC0610B50218C4D5BCA80120FB3C28DFCE03-268435462\r\nServer: nginx\r\nWechatpay-Nonce: 6b8c6025d942cccc9e5c0c31a6097ff0\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: jLFIoDyTQKuPQlqE8DvyFzcOy/AzkfntI+ifJOCuZA1XZSJ4faNweOJq6uydBhhe9Ibzm5dBxCyzAbzY/HBa5XW60JeNvnytb40xwcTvwKorfBLjZGWATbvrEuFdIoYdlFM5jBOXKndRaxYCUDoRmHAJGSHWOUSt+fJMuF1g9LZ77wRzIZlWJvRFtipzXEcu5/Sg1NjXgJcAKNREdVUxbQuEqqzue7d9gy8Fmqcm2cfZhJBXhw4A9FqauPIQ3prUf9nAEGCIFHm+WLHEFTXAXWsyj2Lynbrb8cwJzG2bkd2yesb0pXKJA0WHhWyqNtauezrwIg/tEyyfvgpVw+yDnA==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1770643712\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"code\":\"PARAM_ERROR\",\"detail\":{\"location\":\"body\",\"value\":41},\"message\":\"输入源“/body/batch_remark”映射到值字段“批次备注”字符串规则校验失败,字符数 41,大于最大值 32\"}"}