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>%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\"}"}