Explorar el Código

feat: create mini program virtual payment (#709)

houseme hace 2 años
padre
commit
26b0aacb4c

+ 32 - 1
miniprogram/README.md

@@ -4,7 +4,7 @@
 
 ## 包说明
 
-- analysis 数据分析相关API
+- analysis 数据分析相关 API
 
 ## 快速入门
 
@@ -18,4 +18,35 @@ cfg := &miniConfig.Config{
 }
 miniprogram := wc.GetMiniProgram(cfg)
 miniprogram.GetAnalysis().GetAnalysisDailyRetain()
+```
+
+### 小程序虚拟支付 
+#### `注意:需要传入 Appkey 的值`
+相关文档:[小程序虚拟支付](https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/virtual-payment.html)
+```go
+wc := wechat.NewWechat()
+miniprogram := wc.GetMiniProgram(&miniConfig.Config{
+    AppID:     "xxx",
+    AppSecret: "xxx",
+    AppKey:    "xxx",
+    Cache: cache.NewRedis(&redis.Options{
+        Addr: "",
+    }),
+})
+virtualPayment := miniprogram.GetVirtualPayment()
+virtualPayment.SetSessionKey("xxx")
+// 查询用户余额
+var (
+    res *virtualPayment.QueryUserBalanceResponse
+    err error
+)
+
+if res, err = virtualPayment.QueryUserBalance(context.TODO(), &virtualPayment.QueryUserBalanceRequest{
+    OpenID: "xxx",
+    Env: virtualPayment.EnvProduction,
+    UserIP: "xxx",
+}); err != nil {
+    panic(err)
+}
+
 ```

+ 2 - 1
miniprogram/config/config.go

@@ -1,4 +1,4 @@
-// Package config 小程序config配置
+// Package config 小程序 config 配置
 package config
 
 import (
@@ -9,5 +9,6 @@ import (
 type Config struct {
 	AppID     string `json:"app_id"`     // appid
 	AppSecret string `json:"app_secret"` // appSecret
+	AppKey    string `json:"app_key"`    // appKey
 	Cache     cache.Cache
 }

+ 15 - 9
miniprogram/miniprogram.go

@@ -20,15 +20,16 @@ import (
 	"github.com/silenceper/wechat/v2/miniprogram/tcb"
 	"github.com/silenceper/wechat/v2/miniprogram/urllink"
 	"github.com/silenceper/wechat/v2/miniprogram/urlscheme"
+	"github.com/silenceper/wechat/v2/miniprogram/virtualpayment"
 	"github.com/silenceper/wechat/v2/miniprogram/werun"
 )
 
-// MiniProgram 微信小程序相关API
+// MiniProgram 微信小程序相关 API
 type MiniProgram struct {
 	ctx *context.Context
 }
 
-// NewMiniProgram 实例化小程序API
+// NewMiniProgram 实例化小程序 API
 func NewMiniProgram(cfg *config.Config) *MiniProgram {
 	defaultAkHandle := credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, credential.CacheKeyMiniProgramPrefix, cfg.Cache)
 	ctx := &context.Context{
@@ -38,7 +39,7 @@ func NewMiniProgram(cfg *config.Config) *MiniProgram {
 	return &MiniProgram{ctx}
 }
 
-// SetAccessTokenHandle 自定义access_token获取方式
+// SetAccessTokenHandle 自定义 access_token 获取方式
 func (miniProgram *MiniProgram) SetAccessTokenHandle(accessTokenHandle credential.AccessTokenHandle) {
 	miniProgram.ctx.AccessTokenHandle = accessTokenHandle
 }
@@ -68,17 +69,17 @@ func (miniProgram *MiniProgram) GetBusiness() *business.Business {
 	return business.NewBusiness(miniProgram.ctx)
 }
 
-// GetPrivacy 小程序隐私协议相关API
+// GetPrivacy 小程序隐私协议相关 API
 func (miniProgram *MiniProgram) GetPrivacy() *privacy.Privacy {
 	return privacy.NewPrivacy(miniProgram.ctx)
 }
 
-// GetQRCode 小程序码相关API
+// GetQRCode 小程序码相关 API
 func (miniProgram *MiniProgram) GetQRCode() *qrcode.QRCode {
 	return qrcode.NewQRCode(miniProgram.ctx)
 }
 
-// GetTcb 小程序云开发API
+// GetTcb 小程序云开发 API
 func (miniProgram *MiniProgram) GetTcb() *tcb.Tcb {
 	return tcb.NewTcb(miniProgram.ctx)
 }
@@ -103,7 +104,7 @@ func (miniProgram *MiniProgram) GetContentSecurity() *content.Content {
 	return content.NewContent(miniProgram.ctx)
 }
 
-// GetURLLink 小程序URL Link接口
+// GetURLLink 小程序 URL Link 接口
 func (miniProgram *MiniProgram) GetURLLink() *urllink.URLLink {
 	return urllink.NewURLLink(miniProgram.ctx)
 }
@@ -123,12 +124,17 @@ func (miniProgram *MiniProgram) GetShortLink() *shortlink.ShortLink {
 	return shortlink.NewShortLink(miniProgram.ctx)
 }
 
-// GetSURLScheme 小程序URL Scheme接口
+// GetSURLScheme 小程序 URL Scheme 接口
 func (miniProgram *MiniProgram) GetSURLScheme() *urlscheme.URLScheme {
 	return urlscheme.NewURLScheme(miniProgram.ctx)
 }
 
-// GetOpenAPI openApi管理接口
+// GetOpenAPI openApi 管理接口
 func (miniProgram *MiniProgram) GetOpenAPI() *openapi.OpenAPI {
 	return openapi.NewOpenAPI(miniProgram.ctx)
 }
+
+// GetVirtualPayment 小程序虚拟支付
+func (miniProgram *MiniProgram) GetVirtualPayment() *virtualpayment.VirtualPayment {
+	return virtualpayment.NewVirtualPayment(miniProgram.ctx)
+}

+ 4 - 4
miniprogram/shortlink/shortlink.go

@@ -24,10 +24,10 @@ func NewShortLink(ctx *context.Context) *ShortLink {
 // ShortLinker 请求结构体
 type ShortLinker struct {
 
-	// pageUrl 通过 Short Link 进入的小程序页面路径,必须是已经发布的小程序存在的页面,可携带 query,最大1024个字符
+	// pageUrl 通过 Short Link 进入的小程序页面路径,必须是已经发布的小程序存在的页面,可携带 query,最大 1024 个字符
 	PageURL string `json:"page_url"`
 
-	// pageTitle 页面标题,不能包含违法信息,超过20字符会用... 截断代替
+	// pageTitle 页面标题,不能包含违法信息,超过 20 字符会用... 截断代替
 	PageTitle string `json:"page_title"`
 
 	// isPermanent 生成的 Short Link 类型,短期有效:false,永久有效:true
@@ -67,7 +67,7 @@ func (shortLink *ShortLink) generate(shortLinkParams ShortLinker) (string, error
 	return res.Link, nil
 }
 
-// GenerateShortLinkPermanent 生成永久shortLink
+// GenerateShortLinkPermanent 生成永久 shortLink
 func (shortLink *ShortLink) GenerateShortLinkPermanent(PageURL, pageTitle string) (string, error) {
 	return shortLink.generate(ShortLinker{
 		PageURL:     PageURL,
@@ -76,7 +76,7 @@ func (shortLink *ShortLink) GenerateShortLinkPermanent(PageURL, pageTitle string
 	})
 }
 
-// GenerateShortLinkTemp 生成临时shortLink
+// GenerateShortLinkTemp 生成临时 shortLink
 func (shortLink *ShortLink) GenerateShortLinkTemp(PageURL, pageTitle string) (string, error) {
 	return shortLink.generate(ShortLinker{
 		PageURL:     PageURL,

+ 134 - 0
miniprogram/virtualpayment/constant.go

@@ -0,0 +1,134 @@
+/*
+ *   Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ *   You can obtain one at https://github.com/silenceper/wechat.
+ *
+ */
+
+package virtualpayment
+
+const (
+	// EnvProduction 环境 0-正式环境 1-沙箱环境
+	EnvProduction Env = 0
+	// EnvSandbox 环境 0-正式环境 1-沙箱环境
+	EnvSandbox Env = 1
+)
+
+const (
+	// Success 错误码 0、成功
+	Success ErrCode = 0
+	// SystemError 错误码 -1、系统错误
+	SystemError ErrCode = -1
+	// OpenIDError 错误码 268490001、openid 错误
+	OpenIDError ErrCode = 268490001
+	// RequestParamError 错误码 268490002、请求参数字段错误,具体看 errmsg
+	RequestParamError ErrCode = 268490002
+	// SignError 错误码 268490003、签名错误
+	SignError ErrCode = 268490003
+	// RepeatOperationError 错误码 268490004、重复操作(赠送和代币支付相关接口会返回,表示之前的操作已经成功)
+	RepeatOperationError ErrCode = 268490004
+	// OrderRefundedError 错误码 268490005、订单已经通过 cancel_currency_pay 接口退款,不支持再退款
+	OrderRefundedError ErrCode = 268490005
+	// InsufficientBalanceError 错误码 268490006、代币的退款/支付操作金额不足
+	InsufficientBalanceError ErrCode = 268490006
+	// SensitiveContentError 错误码 268490007、图片或文字存在敏感内容,禁止使用
+	SensitiveContentError ErrCode = 268490007
+	// TokenNotPublishedError 错误码 268490008、代币未发布,不允许进行代币操作
+	TokenNotPublishedError ErrCode = 268490008
+	// SessionKeyExpiredError 错误码 268490009、用户 session_key 不存在或已过期,请重新登录
+	SessionKeyExpiredError ErrCode = 268490009
+	// BillGeneratingError 错误码 268490011、账单数据生成中,请稍后调用本接口获取
+	BillGeneratingError ErrCode = 268490011
+)
+
+const (
+	// OrderStatusInit 订单状态 当前状态 0-订单初始化(未创建成功,不可用于支付)
+	OrderStatusInit OrderStatus = 0
+	// OrderStatusCreated 订单状态 当前状态 1-订单创建成功
+	OrderStatusCreated OrderStatus = 1
+	// OrderStatusPaid 订单状态 当前状态  2-订单已经支付,待发货
+	OrderStatusPaid OrderStatus = 2
+	// OrderStatusDelivering 订单状态 当前状态 3-订单发货中
+	OrderStatusDelivering OrderStatus = 3
+	// OrderStatusDelivered 订单状态 当前状态 4-订单已发货
+	OrderStatusDelivered OrderStatus = 4
+	// OrderStatusRefunded 订单状态 当前状态 5-订单已经退款
+	OrderStatusRefunded OrderStatus = 5
+	// OrderStatusClosed 订单状态 当前状态  6-订单已经关闭(不可再使用)
+	OrderStatusClosed OrderStatus = 6
+	// OrderStatusRefundFailed 订单状态 当前状态 7-订单退款失败
+	OrderStatusRefundFailed OrderStatus = 7
+)
+
+const (
+	// baseSite 基础网址
+	baseSite = "https://api.weixin.qq.com"
+
+	// queryUserBalance 查询虚拟支付余额
+	queryUserBalance = "/xpay/query_user_balance"
+
+	// currencyPay 扣减代币(一般用于代币支付)
+	currencyPay = "/xpay/currency_pay"
+
+	// queryOrder 查询创建的订单(现金单,非代币单)
+	queryOrder = "/xpay/query_order"
+
+	// cancelCurrencyPay 代币支付退款 (currency_pay 接口的逆操作)
+	cancelCurrencyPay = "/xpay/cancel_currency_pay"
+
+	// notifyProvideGoods 通知已经发货完成(只能通知现金单),正常通过 xpay_goods_deliver_notify 消息推送返回成功就不需要调用这个 api 接口。这个接口用于异常情况推送不成功时手动将单改成已发货状态
+	notifyProvideGoods = "/xpay/notify_provide_goods"
+
+	// presentCurrency 代币赠送接口,由于目前不支付按单号查赠送单的功能,所以当需要赠送的时候可以一直重试到返回 0 或者返回 268490004(重复操作)为止
+	presentCurrency = "/xpay/present_currency"
+
+	// downloadBill 下载账单
+	downloadBill = "/xpay/download_bill"
+
+	// refundOrder 退款 对使用 jsapi 接口下的单进行退款
+	refundOrder = "/xpay/refund_order"
+
+	// createWithdrawOrder 创建提现单
+	createWithdrawOrder = "/xpay/create_withdraw_order"
+
+	// queryWithdrawOrder 查询提现单
+	queryWithdrawOrder = "/xpay/query_withdraw_order"
+
+	// startUploadGoods 启动批量上传道具任务
+	startUploadGoods = "/xpay/start_upload_goods"
+
+	// queryUploadGoods 查询批量上传道具任务状态
+	queryUploadGoods = "/xpay/query_upload_goods"
+
+	// startPublishGoods 启动批量发布道具任务
+	startPublishGoods = "/xpay/start_publish_goods"
+
+	// queryPublishGoods 查询批量发布道具任务状态
+	queryPublishGoods = "/xpay/query_publish_goods"
+)
+
+const (
+	// signature user mode signature
+	signature = "signature"
+
+	// paySignature payment signature
+	paySignature = "pay_sig"
+
+	// accessToken access_token authorization tokens
+	accessToken = "access_token"
+
+	// EmptyString empty string
+	EmptyString = ""
+)

+ 32 - 0
miniprogram/virtualpayment/doc.go

@@ -0,0 +1,32 @@
+/*
+ *   Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ *   You can obtain one at https://github.com/silenceper/wechat.
+ *
+ */
+
+// Package virtualpayment mini program virtual payment
+package virtualpayment
+
+import (
+	"github.com/silenceper/wechat/v2/miniprogram/context"
+)
+
+// NewVirtualPayment 实例化小程序虚拟支付 API
+func NewVirtualPayment(ctx *context.Context) *VirtualPayment {
+	return &VirtualPayment{
+		ctx: ctx,
+	}
+}

+ 427 - 0
miniprogram/virtualpayment/domain.go

@@ -0,0 +1,427 @@
+/*
+ *   Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ *   You can obtain one at https://github.com/silenceper/wechat.
+ *
+ */
+
+package virtualpayment
+
+import (
+	"github.com/silenceper/wechat/v2/miniprogram/context"
+	"github.com/silenceper/wechat/v2/util"
+)
+
+// VirtualPayment mini program virtual payment
+// https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/virtual-payment.html#_2-3-%E6%9C%8D%E5%8A%A1%E5%99%A8API
+type VirtualPayment struct {
+	ctx        *context.Context
+	sessionKey string
+}
+
+// Env Environment 0 - Production environment 1 - Sandbox environment
+type Env int
+
+// ErrCode error code
+type ErrCode int
+
+// OrderStatus 订单状态
+type OrderStatus int
+
+// CommonRequest common request parameters
+type CommonRequest struct {
+	OpenID string `json:"openid"` // The user's openID
+	Env    Env    `json:"env"`    // Environment 0 - Production environment 1 - Sandbox environment
+}
+
+// PaymentRequest payment request parameters
+type PaymentRequest struct {
+	SignData  string `json:"sign_data"` // 具体支付参数见 signData, 该参数需以 string 形式传递,例如 signData: '{"offerId":"123","buyQuantity":1,"env":0,"currencyType":"CNY","platform":"android","productId":"testproductId","goodsPrice":10,"outTradeNo":"xxxxxx","attach":"testdata"}'
+	Mode      string `json:"mode"`      // 支付模式,枚举值:short_series_goods: 道具直购,short_series_coin: 代币充值
+	PaySig    string `json:"pay_sig"`   // 支付签名,具体生成方式见下方说明
+	Signature string `json:"signature"` // 用户态签名,具体生成方式见下方说明
+}
+
+// SignData 签名数据
+type SignData struct {
+	OfferID      string `json:"offerId"`             // 在米大师侧申请的应用 id, mp-支付基础配置中的 offerid
+	BuyQuantity  int    `json:"buyQuantity"`         // 购买数量
+	Env          Env    `json:"env"`                 // 环境 0-正式环境 1-沙箱环境
+	CurrencyType string `json:"currencyType"`        // 币种 默认值:CNY 人民币
+	Platform     string `json:"platform,omitempty"`  // 申请接入时的平台,platform 与应用 id 有关 默认值:android 安卓平台
+	ProductID    string `json:"productId,omitempty"` // 道具 ID, **该字段仅 mode=short_series_goods 时可用**
+	GoodsPrice   int    `json:"goodsPrice"`          // 道具单价 (分), **该字段仅 mode=short_series_goods 时可用**, 用来校验价格与后台道具价格是否一致,避免用户在业务商城页看到的价格与实际价格不一致导致投诉
+	OutTradeNo   string `json:"outTradeNo"`          // 业务订单号,每个订单号只能使用一次,重复使用会失败 (极端情况不保证唯一,不建议业务强依赖唯一性). 要求 8-32 个字符内,只能是数字、大小写字母、符号 _-|*@组成,不能以下划线 (_) 开头
+	Attach       string `json:"attach"`              // 透传数据,发货通知时会透传给开发者
+}
+
+// QueryUserBalanceRequest 查询用户代币余额,请求参数
+// 1. 需要用户态签名与支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type QueryUserBalanceRequest struct {
+	CommonRequest
+	UserIP string `json:"user_ip"` // 用户 ip,例如:1.1.1.1
+}
+
+// QueryUserBalanceResponse 查询虚拟支付余额 响应参数
+type QueryUserBalanceResponse struct {
+	util.CommonError
+	Balance        int `json:"balance"`         // 代币总余额,包括有价和赠送部分
+	PresentBalance int `json:"present_balance"` // 赠送账户的代币余额
+	SumSave        int `json:"sum_save"`        // 累计有价货币充值数量
+	SumPresent     int `json:"sum_present"`     // 累计赠送无价货币数量
+	SumBalance     int `json:"sum_balance"`     // 历史总增加的代币金额
+	SumCost        int `json:"sum_cost"`        // 历史总消耗代币金额
+	FirstSaveFlag  int `json:"first_save_flag"` // 是否满足首充活动标记。0:不满足。1:满足
+}
+
+// CurrencyPayRequest 扣减代币(一般用于代币支付)
+// 1. 需要用户态签名与支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type CurrencyPayRequest struct {
+	CommonRequest
+	UserIP     string `json:"user_ip"`     // 用户 ip,例如:1.1.1.1
+	Amount     int    `json:"amount"`      // 支付的代币数量
+	OrderID    string `json:"order_id"`    // 商户订单号,需要保证唯一性
+	PayItem    string `json:"payitem"`     // 物品信息。记录到账户流水中。如:[{"productid":"物品 id", "unit_price": 单价,"quantity": 数量}]
+	Remark     string `json:"remark"`      // 备注信息。需要在账单中展示
+	DeviceType string `json:"device_type"` // 平台类型 1-安卓 2-苹果
+}
+
+// PayItem 物品信息
+type PayItem struct {
+	ProductID string `json:"productid"`  // 物品 id
+	UnitPrice int    `json:"unit_price"` // 单价
+	Quantity  int    `json:"quantity"`   // 数量
+}
+
+// CurrencyPayResponse 扣减代币(一般用于代币支付)响应参数
+type CurrencyPayResponse struct {
+	util.CommonError
+	OrderID           string `json:"order_id"`            // 商户订单号
+	Balance           int    `json:"balance"`             // 总余额,包括有价和赠送部分
+	UsedPresentAmount int    `json:"used_present_amount"` // 使用赠送部分的代币数量
+}
+
+// QueryOrderRequest 查询创建的订单(现金单,非代币单),请求参数
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type QueryOrderRequest struct {
+	CommonRequest
+	OrderID   string `json:"order_id,omitempty"`    // 商户订单号 创建的订单号
+	WxOrderID string `json:"wx_order_id,omitempty"` // 微信内部单号 (与 order_id 二选一)
+}
+
+// OrderItem 订单信息
+type OrderItem struct {
+	OrderID        string      `json:"order_id"`         // 商户订单号
+	CreateTime     int64       `json:"create_time"`      // 订单创建时间
+	UpdateTime     int64       `json:"update_time"`      // 订单更新时间
+	Status         OrderStatus `json:"status"`           // 订单状态 当前状态 0-订单初始化(未创建成功,不可用于支付)1-订单创建成功 2-订单已经支付,待发货 3-订单发货中 4-订单已发货 5-订单已经退款 6-订单已经关闭(不可再使用)7-订单退款失败
+	BizType        int         `json:"biz_type"`         // 业务类型 0-短剧
+	OrderFee       int         `json:"order_fee"`        // 订单金额,单位:分
+	CouponFee      int         `json:"coupon_fee"`       // 优惠金额,单位:分
+	PaidFee        int         `json:"paid_fee"`         // 用户支付金额,单位:分
+	OrderType      int         `json:"order_type"`       // 订单类型 0-支付单 1-退款单
+	RefundFee      int         `json:"refund_fee"`       // 当类型为退款单时表示退款金额,单位分
+	PaidTime       int64       `json:"paid_time"`        // 支付/退款时间,unix秒级时间戳
+	ProvideTime    int64       `json:"provide_time"`     // 发货时间,unix 秒级时间戳
+	BizMeta        string      `json:"biz_meta"`         // 业务自定义数据 订单创建时传的信息
+	EnvType        int         `json:"env_type"`         // 环境类型 1-现网 2-沙箱
+	Token          string      `json:"token"`            // 下单时米大师返回的 token
+	LeftFee        int         `json:"left_fee"`         // 支付单类型时表示此单经过退款还剩余的金额,单位:分
+	WxOrderID      string      `json:"wx_order_id"`      // 微信内部单号
+	ChannelOrderID string      `json:"channel_order_id"` // 渠道订单号,为用户微信支付详情页面上的商户单号
+	WxPayOrderID   string      `json:"wxpay_order_id"`   // 微信支付交易单号,为用户微信支付详情页面上的交易单号
+}
+
+// QueryOrderResponse 查询创建的订单(现金单,非代币单)响应参数
+type QueryOrderResponse struct {
+	util.CommonError
+	Order *OrderItem `json:"order"` // 订单信息
+}
+
+// CancelCurrencyPayRequest 取消订单(现金单,非代币单),请求参数
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type CancelCurrencyPayRequest struct {
+	CommonRequest
+	UserIP     string `json:"user_ip"`      // 用户 ip,例如:1.1.1.1
+	PayOrderID string `json:"pay_order_id"` // 支付单号 代币支付 (调用 currency_pay 接口时) 时传的 order_id
+	OrderID    string `json:"order_id"`     // 本次退款单的单号
+	Amount     int    `json:"amount"`       // 退款金额
+	DeviceType int    `json:"device_type"`  // 平台类型 1-安卓 2-苹果
+}
+
+// CancelCurrencyPayResponse 取消订单(现金单,非代币单)响应参数
+type CancelCurrencyPayResponse struct {
+	util.CommonError
+	OrderID string `json:"order_id"` // 退款订单号
+}
+
+// NotifyProvideGoodsRequest 通知发货,请求参数
+// 通知已经发货完成(只能通知现金单),正常通过 xpay_goods_deliver_notify 消息推送返回成功就不需要调用这个 api 接口。这个接口用于异常情况推送不成功时手动将单改成已发货状态
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type NotifyProvideGoodsRequest struct {
+	OrderID   string `json:"order_id,omitempty"`    // 商户订单号 下单时传的单号
+	WxOrderID string `json:"wx_order_id,omitempty"` // 微信内部单号 (与 order_id 二选一)
+	Env       Env    `json:"env"`                   // 环境 0-正式环境 1-沙箱环境
+}
+
+// NotifyProvideGoodsResponse 通知发货响应参数
+type NotifyProvideGoodsResponse struct {
+	util.CommonError
+}
+
+// PresentCurrencyRequest 赠送代币,请求参数
+// 代币赠送接口,由于目前不支付按单号查赠送单的功能,所以当需要赠送的时候可以一直重试到返回 0 或者返回 268490004(重复操作)为止
+// 1. 需要用户态签名与支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type PresentCurrencyRequest struct {
+	CommonRequest
+	OrderID    string `json:"order_id"`    // 赠送单号,商户订单号,需要保证唯一性
+	Amount     int    `json:"amount"`      // 赠送的代币数量
+	DeviceType string `json:"device_type"` // 平台类型 1-安卓 2-苹果
+}
+
+// PresentCurrencyResponse 赠送代币响应参数
+type PresentCurrencyResponse struct {
+	util.CommonError
+	Balance        int    `json:"balance"`         // 赠送后用户的代币余额
+	OrderID        string `json:"order_id"`        // 赠送单号
+	PresentBalance int    `json:"present_balance"` // 用户收到的总赠送金额
+}
+
+// DownloadBillRequest 下载账单,请求参数
+// 用于下载小程序账单,第一次调用触发生成下载 url,可以间隔轮训来获取最终生成的下载 url。账单中金额相关字段是以分为单位。
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type DownloadBillRequest struct {
+	BeginDs string `json:"begin_ds"` // 账单开始日期,格式为 yyyymmdd 起始时间(如 20230801)
+	EndDs   string `json:"end_ds"`   // 账单结束日期,格式为 yyyymmdd 结束时间(如 20230801)
+}
+
+// DownloadBillResponse 下载账单响应参数
+type DownloadBillResponse struct {
+	util.CommonError
+	URL string `json:"url"` // 账单下载地址
+}
+
+// RefundOrderRequest 退款,请求参数
+// 对使用 jsapi 接口下的单进行退款
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type RefundOrderRequest struct {
+	CommonRequest
+	OrderID       string `json:"order_id"`        // 商户订单号,需要保证唯一性
+	WxOrderID     string `json:"wx_order_id"`     // 微信内部单号 (与 order_id 二选一)
+	RefundOrderID string `json:"refund_order_id"` // 退款单号,本次退款时需要传的单号,长度为 [8,32],字符只允许使用字母、数字、'_'、'-'
+	LeftFee       int    `json:"left_fee"`        // 退款金额,单位:分 当前单剩余可退金额,单位分,可以通过调用 query_order 接口查到
+	RefundFee     int    `json:"refund_fee"`      // 退款金额,单位:分 需要 (0,left_fee] 之间
+	BizMeta       string `json:"biz_meta"`        // 商家自定义数据,传入后可在 query_order 接口查询时原样返回,长度需要 [0,1024]
+	RefundReason  string `json:"refund_reason"`   // 退款原因,当前仅支持以下值 0-暂无描述 1-产品问题,影响使用或效果不佳 2-售后问题,无法满足需求 3-意愿问题,用户主动退款 4-价格问题 5:其他原因
+	ReqFrom       string `json:"req_from"`        // 退款来源,当前仅支持以下值 1-人工客服退款,即用户电话给客服,由客服发起退款流程 2-用户自己发起退款流程 3-其它
+}
+
+// RefundOrderResponse 退款响应参数
+type RefundOrderResponse struct {
+	util.CommonError
+	RefundOrderID   string `json:"refund_order_id"`    // 退款单号
+	RefundWxOrderID string `json:"refund_wx_order_id"` // 退款单的微信侧单号
+	PayOrderID      string `json:"pay_order_id"`       // 该退款单对应的支付单单号
+	PayWxOrderID    string `json:"pay_wx_order_id"`    // 该退款单对应的支付单微信侧单号
+}
+
+// CreateWithdrawOrderRequest 创建提现单,请求参数
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type CreateWithdrawOrderRequest struct {
+	WithdrawNO     string `json:"withdraw_no"`     // 提现单单号,长度为 [8,32],字符只允许使用字母、数字、'_'、'-'
+	WithdrawAmount string `json:"withdraw_amount"` // 提现的金额,单位元,例如提现 1 分钱请使用 0.01
+	Env            Env    `json:"env"`             // 环境 0-正式环境 1-沙箱环境
+}
+
+// CreateWithdrawOrderResponse 创建提现单响应参数
+type CreateWithdrawOrderResponse struct {
+	util.CommonError
+	WithdrawNO   string `json:"withdraw_no"`    // 提现单单号
+	WxWithdrawNO string `json:"wx_withdraw_no"` // 提现单的微信侧单号
+}
+
+// QueryWithdrawOrderRequest 查询提现单,请求参数
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type QueryWithdrawOrderRequest struct {
+	WithdrawNO string `json:"withdraw_no"` // 提现单单号,长度为 [8,32],字符只允许使用字母、数字、'_'、'-' (与 wx_withdraw_no 二选一)
+	Env        Env    `json:"env"`         // 环境 0-正式环境 1-沙箱环境
+}
+
+// QueryWithdrawOrderResponse 查询提现单响应参数
+type QueryWithdrawOrderResponse struct {
+	util.CommonError
+	WithdrawNO               string `json:"withdraw_no"`                // 提现单单号
+	Status                   int    `json:"status"`                     // 提现单的微信侧单号 1-创建成功,提现中 2-提现成功 3-提现失败
+	WithdrawAmount           string `json:"withdraw_amount"`            // 提现的金额,单位元,例如提现 1 分钱请使用 0.01
+	WxWithdrawNo             string `json:"wx_withdraw_no"`             // 提现单的微信侧单号
+	WithdrawSuccessTimestamp int64  `json:"withdraw_success_timestamp"` // 提现单成功的秒级时间戳,unix 秒级时间戳
+	CreateTime               string `json:"create_time"`                // 提现单创建时间
+	FailReason               string `json:"failReason"`                 // 提现失败的原因
+}
+
+// StartUploadGoodsRequest 启动批量上传道具任务,请求参数
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type StartUploadGoodsRequest struct {
+	UploadItem []*UploadItem `json:"upload_item"` // 道具信息
+	Env        Env           `json:"env"`         // 环境 0-正式环境 1-沙箱环境
+}
+
+// UploadItem 道具信息
+type UploadItem struct {
+	ID           string `json:"id"`                      // 道具 id,长度 (0,64],字符只允许使用字母、数字、'_'、'-'
+	Name         string `json:"name"`                    // 道具名称,长度 (0,1024]
+	Price        int    `json:"price"`                   // 道具单价,单位分,需要大于 0
+	Remark       string `json:"remark"`                  // 道具备注,长度 (0,1024]
+	ItemURL      string `json:"item_url"`                // 道具图片的 url 地址,当前仅支持 jpg,png 等格式
+	UploadStatus int    `json:"upload_status,omitempty"` // 上传状态 0-上传中 1-id 已经存在 2-上传成功 3-上传失败
+	ErrMsg       string `json:"errmsg,omitempty"`        // 上传失败的原因
+}
+
+// StartUploadGoodsResponse 启动批量上传道具任务响应参数
+type StartUploadGoodsResponse struct {
+	util.CommonError
+}
+
+// QueryUploadGoodsRequest 查询批量上传道具任务,请求参数
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type QueryUploadGoodsRequest struct {
+	Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
+}
+
+// QueryUploadGoodsResponse 查询批量上传道具任务响应参数
+type QueryUploadGoodsResponse struct {
+	util.CommonError
+	UploadItem []*UploadItem `json:"upload_item"` // 道具信息列表
+	Status     int           `json:"status"`      // 任务状态 0-无任务在运行 1-任务运行中 2-上传失败或部分失败(上传任务已经完成)3-上传成功
+}
+
+// StartPublishGoodsRequest 启动批量发布道具任务,请求参数
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type StartPublishGoodsRequest struct {
+	Env         Env            `json:"env"`          // 环境 0-正式环境 1-沙箱环境
+	PublishItem []*PublishItem `json:"publish_item"` // 道具信息 发布的商品列表
+}
+
+// PublishItem 道具信息
+type PublishItem struct {
+	ID            string `json:"id"`                       // 道具 id,添加到开发环境时传的道具 id,长度 (0,64],字符只允许使用字母、数字、'_'、'-'
+	PublishStatus int    `json:"publish_status,omitempty"` // 发布状态 0-上传中 1-id 已经存在 2-发布成功 3-发布失败
+	ErrMsg        string `json:"errmsg,omitempty"`         // 发布失败的原因
+}
+
+// StartPublishGoodsResponse 启动批量发布道具任务响应参数
+type StartPublishGoodsResponse struct {
+	util.CommonError
+}
+
+// QueryPublishGoodsRequest 查询批量发布道具任务,请求参数
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type QueryPublishGoodsRequest struct {
+	Env Env `json:"env"` // 环境 0-正式环境 1-沙箱环境
+}
+
+// QueryPublishGoodsResponse 查询批量发布道具任务响应参数
+type QueryPublishGoodsResponse struct {
+	util.CommonError
+	PublishItem []*PublishItem `json:"publish_item"` // 道具信息列表
+	Status      int            `json:"status"`       // 任务状态 0-无任务在运行 1-任务运行中 2-上传失败或部分失败(上传任务已经完成)3-上传成功
+
+}
+
+// AsyncXPayGoodsDeliverNotifyRequest 异步通知发货,请求参数
+// 1. 使用支付签名
+// POST,请求参数为 json 字符串,Content-Type 为 application/json
+type AsyncXPayGoodsDeliverNotifyRequest struct {
+	ToUserName    string         `json:"ToUserName"`    // 小程序的原始 ID
+	FromUserName  string         `json:"FromUserName"`  // 发送方帐号(一个 OpenID)该事件消息的 openid,道具发货场景固定为微信官方的 openid
+	CreateTime    int            `json:"CreateTime"`    // 消息发送时间(整型)
+	MsgType       string         `json:"MsgType"`       // 消息类型,此时固定为:event
+	Event         string         `json:"Event"`         // 事件类型,此时固定为:xpay_goods_deliver_notify
+	Openid        string         `json:"openid"`        // 用户 openid
+	OutTradeNo    string         `json:"OutTradeNo"`    // 业务订单号
+	Env           Env            `json:"env"`           // 环境 0-正式环境 1-沙箱环境
+	WechatPayInfo *WeChatPayInfo `json:"WechatPayInfo"` // 微信支付订单信息
+	GoodsInfo     *GoodsInfo     `json:"GoodsInfo"`     // 道具信息
+}
+
+// WeChatPayInfo 微信支付信息 非微信支付渠道可能没有
+type WeChatPayInfo struct {
+	MchOrderNo    string `json:"MchOrderNo"`    // 商户订单号
+	TransactionID string `json:"TransactionId"` // 微信支付订单号
+}
+
+// GoodsInfo 道具参数信息
+type GoodsInfo struct {
+	ProductID   string `json:"ProductId"`   // 道具 ID
+	Quantity    int    `json:"Quantity"`    // 数量
+	OrigPrice   int    `json:"OrigPrice"`   // 物品原始价格(单位:分)
+	ActualPrice int    `json:"ActualPrice"` // 物品实际支付价格(单位:分)
+	Attach      string `json:"Attach"`      // 透传信息
+}
+
+// AsyncXPayGoodsDeliverNotifyResponse 异步通知发货响应参数
+type AsyncXPayGoodsDeliverNotifyResponse struct {
+	util.CommonError
+}
+
+// AsyncXPayCoinPayNotifyRequest 异步通知代币支付推送,请求参数
+type AsyncXPayCoinPayNotifyRequest struct {
+	ToUserName    string         `json:"ToUserName"`    // 小程序的原始 ID
+	FromUserName  string         `json:"FromUserName"`  // 发送方帐号(一个 OpenID)该事件消息的 openid,道具发货场景固定为微信官方的 openid
+	CreateTime    int            `json:"CreateTime"`    // 消息发送时间(整型)
+	MsgType       string         `json:"MsgType"`       // 消息类型,此时固定为:event
+	Event         string         `json:"Event"`         // 事件类型,此时固定为:xpay_goods_deliver_notify
+	Openid        string         `json:"openid"`        // 用户 openid
+	OutTradeNo    string         `json:"OutTradeNo"`    // 业务订单号
+	Env           Env            `json:"env"`           // 环境 0-正式环境 1-沙箱环境
+	WechatPayInfo *WeChatPayInfo `json:"WechatPayInfo"` // 微信支付订单信息
+	CoinInfo      *CoinInfo      `json:"GoodsInfo"`     // 道具信息
+}
+
+// CoinInfo 代币信息
+type CoinInfo struct {
+	Quantity    int    `json:"Quantity"`    // 数量
+	OrigPrice   int    `json:"OrigPrice"`   // 物品原始价格(单位:分)
+	ActualPrice int    `json:"ActualPrice"` // 物品实际支付价格(单位:分)
+	Attach      string `json:"Attach"`      // 透传信息
+}
+
+// AsyncXPayCoinPayNotifyResponse 异步通知代币支付推送响应参数
+type AsyncXPayCoinPayNotifyResponse struct {
+	util.CommonError
+}
+
+// URLParams url parameter
+type URLParams struct {
+	Path        string `json:"path"`
+	AccessToken string `json:"access_token"`
+	PaySign     string `json:"paySign"`
+	Signature   string `json:"signature"`
+	Content     string `json:"content"`
+}

+ 558 - 0
miniprogram/virtualpayment/virtualpayment.go

@@ -0,0 +1,558 @@
+/*
+ *   Copyright silenceper/wechat Author(https://silenceper.com/wechat/). All Rights Reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ *   You can obtain one at https://github.com/silenceper/wechat.
+ *
+ */
+
+package virtualpayment
+
+import (
+	"context"
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"strings"
+
+	"github.com/silenceper/wechat/v2/util"
+)
+
+// SetSessionKey 设置 sessionKey
+func (s *VirtualPayment) SetSessionKey(sessionKey string) {
+	s.sessionKey = sessionKey
+}
+
+// QueryUserBalance 查询虚拟支付余额
+func (s *VirtualPayment) QueryUserBalance(ctx context.Context, in *QueryUserBalanceRequest) (out *QueryUserBalanceResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:    queryUserBalance,
+			Content: string(jsonByte),
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "QueryUserBalance"); err != nil {
+		return
+	}
+
+	return
+}
+
+// CurrencyPay currency pay 扣减代币(一般用于代币支付)
+func (s *VirtualPayment) CurrencyPay(ctx context.Context, in *CurrencyPayRequest) (out *CurrencyPayResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:    currencyPay,
+			Content: string(jsonByte),
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "CurrencyPay"); err != nil {
+		return
+	}
+
+	return
+}
+
+// QueryOrder 查询创建的订单(现金单,非代币单)
+func (s *VirtualPayment) QueryOrder(ctx context.Context, in *QueryOrderRequest) (out *QueryOrderResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      queryOrder,
+			Signature: EmptyString,
+			Content:   string(jsonByte),
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "QueryOrder"); err != nil {
+		return
+	}
+
+	return
+}
+
+// CancelCurrencyPay 取消订单 代币支付退款 (currency_pay 接口的逆操作)
+func (s *VirtualPayment) CancelCurrencyPay(ctx context.Context, in *CancelCurrencyPayRequest) (out *CancelCurrencyPayResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:    cancelCurrencyPay,
+			Content: string(jsonByte),
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "CancelCurrencyPay"); err != nil {
+		return
+	}
+
+	return
+}
+
+// NotifyProvideGoods 通知发货
+// 通知已经发货完成(只能通知现金单),正常通过 xpay_goods_deliver_notify 消息推送返回成功就不需要调用这个 api 接口。这个接口用于异常情况推送不成功时手动将单改成已发货状态
+func (s *VirtualPayment) NotifyProvideGoods(ctx context.Context, in *NotifyProvideGoodsRequest) (out *NotifyProvideGoodsResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      notifyProvideGoods,
+			Content:   string(jsonByte),
+			Signature: EmptyString,
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "NotifyProvideGoods"); err != nil {
+		return
+	}
+
+	return
+}
+
+// PresentCurrency 代币赠送接口,由于目前不支付按单号查赠送单的功能,所以当需要赠送的时候可以一直重试到返回 0 或者返回 268490004(重复操作)为止
+func (s *VirtualPayment) PresentCurrency(ctx context.Context, in *PresentCurrencyRequest) (out *PresentCurrencyResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      presentCurrency,
+			Content:   string(jsonByte),
+			Signature: EmptyString,
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "PresentCurrency"); err != nil {
+		return
+	}
+
+	return
+}
+
+// DownloadBill 下载订单交易账单
+func (s *VirtualPayment) DownloadBill(ctx context.Context, in *DownloadBillRequest) (out *DownloadBillResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      downloadBill,
+			Content:   string(jsonByte),
+			Signature: EmptyString,
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "DownloadBill"); err != nil {
+		return
+	}
+
+	return
+}
+
+// RefundOrder 退款 对使用 jsapi 接口下的单进行退款
+func (s *VirtualPayment) RefundOrder(ctx context.Context, in *RefundOrderRequest) (out *RefundOrderResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      refundOrder,
+			Content:   string(jsonByte),
+			Signature: EmptyString,
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "RefundOrder"); err != nil {
+		return
+	}
+
+	return
+}
+
+// CreateWithdrawOrder 创建提现单
+func (s *VirtualPayment) CreateWithdrawOrder(ctx context.Context, in *CreateWithdrawOrderRequest) (out *CreateWithdrawOrderResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      createWithdrawOrder,
+			Content:   string(jsonByte),
+			Signature: EmptyString,
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "CreateWithdrawOrder"); err != nil {
+		return
+	}
+
+	return
+}
+
+// QueryWithdrawOrder 查询提现单
+func (s *VirtualPayment) QueryWithdrawOrder(ctx context.Context, in *QueryWithdrawOrderRequest) (out *QueryWithdrawOrderResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      queryWithdrawOrder,
+			Content:   string(jsonByte),
+			Signature: EmptyString,
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "QueryWithdrawOrder"); err != nil {
+		return
+	}
+
+	return
+}
+
+// StartUploadGoods 开始上传商品
+func (s *VirtualPayment) StartUploadGoods(ctx context.Context, in *StartUploadGoodsRequest) (out *StartUploadGoodsResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      startUploadGoods,
+			Content:   string(jsonByte),
+			Signature: EmptyString,
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "StartUploadGoods"); err != nil {
+		return
+	}
+
+	return
+}
+
+// QueryUploadGoods 查询上传商品
+func (s *VirtualPayment) QueryUploadGoods(ctx context.Context, in *QueryUploadGoodsRequest) (out *QueryUploadGoodsResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      queryUploadGoods,
+			Content:   string(jsonByte),
+			Signature: EmptyString,
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "QueryUploadGoods"); err != nil {
+		return
+	}
+
+	return
+}
+
+// StartPublishGoods 开始发布商品
+func (s *VirtualPayment) StartPublishGoods(ctx context.Context, in *StartPublishGoodsRequest) (out *StartPublishGoodsResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      startPublishGoods,
+			Content:   string(jsonByte),
+			Signature: EmptyString,
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "StartPublishGoods"); err != nil {
+		return
+	}
+
+	return
+}
+
+// QueryPublishGoods 查询发布商品
+func (s *VirtualPayment) QueryPublishGoods(ctx context.Context, in *QueryPublishGoodsRequest) (out *QueryPublishGoodsResponse, err error) {
+	var jsonByte []byte
+	if jsonByte, err = json.Marshal(in); err != nil {
+		return
+	}
+
+	var (
+		params = URLParams{
+			Path:      queryPublishGoods,
+			Content:   string(jsonByte),
+			Signature: EmptyString,
+		}
+		address string
+	)
+	if address, err = s.requestAddress(params); err != nil {
+		return
+	}
+
+	var response []byte
+	if response, err = util.PostJSONContext(ctx, address, in); err != nil {
+		return
+	}
+
+	// 使用通用方法返回错误
+	if err = util.DecodeWithError(response, out, "QueryPublishGoods"); err != nil {
+		return
+	}
+
+	return
+}
+
+// hmacSha256 hmac sha256
+func (s *VirtualPayment) hmacSha256(key, data string) string {
+	h := hmac.New(sha256.New, []byte(key))
+	h.Write([]byte(data))
+	return hex.EncodeToString(h.Sum(nil))
+}
+
+// PaySign pay sign
+func (s *VirtualPayment) PaySign(url, data string) (string, error) {
+	if strings.TrimSpace(s.ctx.Config.AppKey) == "" {
+		return "", errors.New("appKey is empty")
+	}
+	return s.hmacSha256(s.ctx.Config.AppKey, url+"&"+data), nil
+}
+
+// Signature user signature
+func (s *VirtualPayment) Signature(data string) (string, error) {
+	if strings.TrimSpace(s.sessionKey) == "" {
+		return "", errors.New("sessionKey is empty")
+	}
+	return s.hmacSha256(s.sessionKey, data), nil
+}
+
+// PaySignature pay sign and signature
+func (s *VirtualPayment) PaySignature(url, data string) (paySign, signature string, err error) {
+	if paySign, err = s.PaySign(url, data); err != nil {
+		return
+	}
+	if signature, err = s.Signature(data); err != nil {
+		return
+	}
+	return
+}
+
+// requestURL .组合 URL
+func (s *VirtualPayment) requestAddress(params URLParams) (url string, err error) {
+	switch params.Path {
+	case queryUserBalance:
+	case currencyPay:
+	case cancelCurrencyPay:
+		if params.PaySign, params.Signature, err = s.PaySignature(params.Path, params.Content); err != nil {
+			return
+		}
+	case queryOrder:
+	case notifyProvideGoods:
+	case presentCurrency:
+	case downloadBill:
+	case refundOrder:
+	case createWithdrawOrder:
+	case queryWithdrawOrder:
+	case startUploadGoods:
+	case queryUploadGoods:
+	case startPublishGoods:
+	case queryPublishGoods:
+		if params.PaySign, err = s.PaySign(params.Path, params.Content); err != nil {
+			return
+		}
+	default:
+		err = errors.New("path is not exist")
+		return
+	}
+
+	if params.AccessToken, err = s.ctx.GetAccessToken(); err != nil {
+		return
+	}
+
+	url = baseSite + params.Path + "?" + accessToken + "=" + params.AccessToken
+	if params.PaySign != EmptyString {
+		url += "&" + paySignature + "=" + params.PaySign
+	}
+	if params.Signature != EmptyString {
+		url += "&" + signature + "=" + params.Signature
+	}
+	return
+}

+ 5 - 5
officialaccount/material/material.go

@@ -27,7 +27,7 @@ const (
 	PermanentMaterialTypeImage PermanentMaterialType = "image"
 	// PermanentMaterialTypeVideo 永久素材视频类型(video)
 	PermanentMaterialTypeVideo PermanentMaterialType = "video"
-	// PermanentMaterialTypeVoice 永久素材语音类型 (voice)
+	// PermanentMaterialTypeVoice 永久素材语音类型(voice)
 	PermanentMaterialTypeVoice PermanentMaterialType = "voice"
 	// PermanentMaterialTypeNews 永久素材图文类型(news)
 	PermanentMaterialTypeNews PermanentMaterialType = "news"
@@ -278,7 +278,7 @@ type ArticleList struct {
 	Item       []ArticleListItem `json:"item"`
 }
 
-// ArticleListItem 用于ArticleList的item节点
+// ArticleListItem 用于 ArticleList  item 节点
 type ArticleListItem struct {
 	MediaID    string             `json:"media_id"`
 	Content    ArticleListContent `json:"content"`
@@ -287,14 +287,14 @@ type ArticleListItem struct {
 	UpdateTime int64              `json:"update_time"`
 }
 
-// ArticleListContent 用于ArticleListItem的content节点
+// ArticleListContent 用于 ArticleListItem  content 节点
 type ArticleListContent struct {
 	NewsItem   []Article `json:"news_item"`
 	UpdateTime int64     `json:"update_time"`
 	CreateTime int64     `json:"create_time"`
 }
 
-// reqBatchGetMaterial BatchGetMaterial请求参数
+// reqBatchGetMaterial BatchGetMaterial 请求参数
 type reqBatchGetMaterial struct {
 	Type   PermanentMaterialType `json:"type"`
 	Count  int64                 `json:"count"`
@@ -337,7 +337,7 @@ type ResMaterialCount struct {
 	NewsCount  int64 `json:"news_count"`  // 图文总数量
 }
 
-// GetMaterialCount 获取素材总数.
+// GetMaterialCount 获取素材总数
 func (material *Material) GetMaterialCount() (res ResMaterialCount, err error) {
 	var accessToken string
 	accessToken, err = material.GetAccessToken()

+ 3 - 3
util/error.go

@@ -6,7 +6,7 @@ import (
 	"reflect"
 )
 
-// CommonError 微信返回的通用错误json
+// CommonError 微信返回的通用错误 json
 type CommonError struct {
 	apiName string
 	ErrCode int64  `json:"errcode"`
@@ -17,7 +17,7 @@ func (c *CommonError) Error() string {
 	return fmt.Sprintf("%s Error , errcode=%d , errmsg=%s", c.apiName, c.ErrCode, c.ErrMsg)
 }
 
-// NewCommonError 新建CommonError错误,对于无errcode和errmsg的返回也可以返回该通用错误
+// NewCommonError 新建 CommonError 错误,对于无 errcode  errmsg 的返回也可以返回该通用错误
 func NewCommonError(apiName string, code int64, msg string) *CommonError {
 	return &CommonError{
 		apiName: apiName,
@@ -26,7 +26,7 @@ func NewCommonError(apiName string, code int64, msg string) *CommonError {
 	}
 }
 
-// DecodeWithCommonError 将返回值按照CommonError解析
+// DecodeWithCommonError 将返回值按照 CommonError 解析
 func DecodeWithCommonError(response []byte, apiName string) (err error) {
 	var commError CommonError
 	err = json.Unmarshal(response, &commError)

+ 3 - 3
util/http.go

@@ -100,7 +100,7 @@ func PostJSON(uri string, obj interface{}) ([]byte, error) {
 	return PostJSONContext(context.Background(), uri, obj)
 }
 
-// PostJSONWithRespContentType post json数据请求,且返回数据类型
+// PostJSONWithRespContentType post json 数据请求,且返回数据类型
 func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, error) {
 	jsonBuf := new(bytes.Buffer)
 	enc := json.NewEncoder(jsonBuf)
@@ -216,7 +216,7 @@ func PostXML(uri string, obj interface{}) ([]byte, error) {
 	return io.ReadAll(response.Body)
 }
 
-// httpWithTLS CA证书
+// httpWithTLS CA 证书
 func httpWithTLS(rootCa, key string) (*http.Client, error) {
 	var client *http.Client
 	certData, err := os.ReadFile(rootCa)
@@ -235,7 +235,7 @@ func httpWithTLS(rootCa, key string) (*http.Client, error) {
 	return client, nil
 }
 
-// pkcs12ToPem 将Pkcs12转成Pem
+// pkcs12ToPem 将 Pkcs12 转成 Pem
 func pkcs12ToPem(p12 []byte, password string) tls.Certificate {
 	blocks, err := pkcs12.ToPEM(p12, password)
 	defer func() {

+ 1 - 1
wechat.go

@@ -40,7 +40,7 @@ func NewWechat() *Wechat {
 	return &Wechat{}
 }
 
-// SetCache 设置cache
+// SetCache 设置 cache
 func (wc *Wechat) SetCache(cache cache.Cache) {
 	wc.cache = cache
 }