优化提现记录页面,新增“领取零钱”按钮以支持用户提现操作,并更新相关API以获取转账信息。调整页面布局以提升用户体验,同时确保状态字段的一致性和可读性。

This commit is contained in:
2026-02-09 21:29:52 +08:00
parent 0e716cbc6e
commit ae35460622
16 changed files with 528 additions and 566 deletions

View File

@@ -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' })
}
}
})

View File

@@ -15,7 +15,10 @@
<text class="amount">¥{{item.amount}}</text>
<text class="time">{{item.createdAt}}</text>
</view>
<text class="status status-{{item.statusRaw}}">{{item.status}}</text>
<view class="item-right">
<text class="status status-{{item.statusRaw}}">{{item.status}}</text>
<button wx:if="{{item.canReceive}}" class="btn-receive" data-id="{{item.id}}" bindtap="onReceiveTap">领取零钱</button>
</view>
</view>
</view>
</view>

View File

@@ -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; }

View File

@@ -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) {

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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/notifyv3 支付回调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)
}
// 处理分销佣金

View File

@@ -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) {

View File

@@ -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)
}
// ----- 提现 -----

View File

@@ -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 := "<xml>"
for k, v := range data {
xml += fmt.Sprintf("<%s><![CDATA[%s]]></%s>", k, v, k)
}
xml += "</xml>"
return xml
}
func xmlToMap(xmlStr string) map[string]string {
result := make(map[string]string)
// 简单的 XML 解析(仅支持 <key><![CDATA[value]]></key> 和 <key>value</key> 格式)
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] == "<![CDATA[" {
inCDATA = true
i += 8 // skip "![CDATA["
continue
}
inTag = true
key = ""
continue
}
if ch == '>' {
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 生成订单号

View File

@@ -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("转账回调需要完整实现")
}

Binary file not shown.

View File

@@ -1 +0,0 @@
exit status 1exit status 1exit status 1exit status 1

Binary file not shown.

View File

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