diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e0414a6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 黑白配 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 7d597b3..7adce98 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,131 @@ -# wxpay +# 微信支付 SDK +- [x] V2 版支付(商户/服务商) +- [x] V2 版分账(商户/服务商) +- [x] V2 版企业付款到零钱 +- [x] V3 版支付即服务 + +![go 1.15](https://img.shields.io/badge/go-1.15-green) +[![go.dev doc](https://img.shields.io/badge/go.dev-doc-green)](https://pkg.go.dev/github.com/wleven/wxpay) +[![GitHub license](https://img.shields.io/github/license/wleven/wxpay)](https://github.com/wleven/wxpay/blob/master/LICENSE) + +- [微信支付 SDK](#微信支付-sdk) + - [安装包](#安装包) + - [查看文档](#查看文档) + - [V2 版本下单接口](#v2-版本下单接口) + - [V2 版本分账接口](#v2-版本分账接口) + - [V2 版本企业付款到零钱](#v2-版本企业付款到零钱) + - [V3 版本支付即服务接口](#v3-版本支付即服务接口) + +## 安装包 + +```golang +go get -u github.com/wleven/wxpay +``` + +## 查看文档 + +```golang +// 执行命令 +godoc -http=:8888 -play +// 浏览器打开文档 +http://127.0.0.1:8888/pkg/github.com/wleven/wxpay/ +``` + +## V2 版本下单接口 + +```golang +config := entity.PayConfig{ + // 传入支付初始化参数 + AppID string // 商户/服务商 AppId(公众号/小程序) + MchID string // 商户/服务商 商户号 + SubAppID string // 子商户公众号ID + SubMchID string // 子商户商户号 + PayNotify string // 支付结果回调地址 + RefundNotify string // 退款结果回调地址 + Secret string // 微信支付密钥 + APIClientPath APIClientPath // API证书路径,使用V3接口必传 + SerialNo string // 证书编号,使用V3接口必传 +} + +wxpay := WXPay.Init(config) +// 统一下单 +if data, err := wxpay.V2.UnifiedOrder(V2.UnifiedOrder{/* 传入参数 */}); err == nil { +} +// 小程序支付 +if data, err := wxpay.V2.WxAppPay(V2.UnifiedOrder{/* 传入参数 */}); err == nil { +} +// APP支付 +if data, err := wxpay.V2.WxAppAppPay(V2.UnifiedOrder{/* 传入参数 */}); err == nil { +} +// H5支付 +if data, err := wxpay.V2.WxH5Pay(V2.UnifiedOrder{/* 传入参数 */}); err == nil { +} +// 付款码支付 +if data, err := wxpay.V2.Micropay(V2.Micropay{/* 传入参数 */}); err == nil { +} +// 关闭订单 +if data, err := wxpay.V2.CloseOrder("1111"); err == nil { +} +// 撤销订单 +if data, err := wxpay.V2.ReverseOrder(V2.ReverseOrder{/* 传入参数 */}); err == nil { +} +// 查询订单 +if data, err := wxpay.V2.OrderQuery(V2.OrderQuery{/* 传入参数 */}); err == nil { +} +// 申请退款 +if data, err := wxpay.V2.Refund(V2.Refund{/* 传入参数 */}); err == nil { +} +// 查询退款 +if data, err := wxpay.V2.RefundQuery(V2.RefundQuery{/* 传入参数 */}); err == nil { +} + +``` + +## V2 版本分账接口 + +```golang +// 添加分账接收方 +if data, err := wxpay.V2.ProfitSharingAddReceiver(V2.Receiver{/* 传入参数 */}); err == nil { +} +// 删除分账接收方 +if data, err := wxpay.V2.ProfitSharingRemoveReceiver(V2.Receiver{/* 传入参数 */}); err == nil { +} +// 发起分账 第二个参数options为multi为多次分账 默认为单次 +if data, err := wxpay.V2.ProfitSharing(V2.ProfitSharing{/* 传入参数 */},""); err == nil { +} +// 完成分账 +if data, err := wxpay.V2.ProfitSharingFinish(V2.ProfitSharingFinish{/* 传入参数 */}); err == nil { +} +// 查询分账结果 +if data, err := wxpay.V2.ProfitSharingQuery(V2.ProfitSharingQuery{/* 传入参数 */}); err == nil { +} +// 分账回退 +if data, err := wxpay.V2.ProfitSharingReturn(V2.ProfitSharingReturn{/* 传入参数 */}); err == nil { +} +// 分账回退结果查询 +if data, err := wxpay.V2.ProfitSharingReturnQuery(V2.ProfitSharingReturnQuery{/* 传入参数 */}); err == nil { +} + +``` + +## V2 版本企业付款到零钱 + +```golang +if data, err := wxpay.V2.Transfers(V2.Transfers{/* 传入参数 */}); err == nil { +} +``` + +## V3 版本支付即服务接口 + +```golang +// 注册服务人员 +if data, err := wxpay.V3.SmartGuide.Register(smartGuide.Register{/* 传入参数 */}); err == nil { +} +// 分配服务人员 +if data, err := wxpay.V3.SmartGuide.Assign(smartGuide.Assign{/* 传入参数 */}); err == nil { +} +// 查询服务人员 +if data, err := wxpay.V3.SmartGuide.Query(smartGuide.Query{/* 传入参数 */}); err == nil { +} +``` diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..fa8bdd6 --- /dev/null +++ b/doc.go @@ -0,0 +1,2 @@ +// Package pay 微信支付SDK,支持V2/V3版商户和服务商接口,持续更新 +package WXPay diff --git a/global/index.go b/global/index.go new file mode 100644 index 0000000..6ffaa4c --- /dev/null +++ b/global/index.go @@ -0,0 +1,34 @@ +// @Time : 2020/7/9 14:23 +// @Author : 黑白配 +// @File : index.go +// @PackageName:global +// @Description: + +package global + +import ( + "git.ouxuan.net/3136352472/wxpay/src/config" + "git.ouxuan.net/3136352472/wxpay/src/entity" +) + +var V3 = config.V3{ + MchID: "", + ClientKeyPath: "", + SerialNo: "", +} + +var V2 = entity.PayConfig{ + AppID: "", + MchID: "", + SubAppID: "", + SubMchID: "", + PayNotify: "", + RefundNotify: "", + Secret: "", + APIClientPath: entity.APIClientPath{ + Cert: "", + Key: "", + Root: "", + }, + SerialNo: "", +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3c641d0 --- /dev/null +++ b/main.go @@ -0,0 +1,34 @@ +// @Time : 2020/7/9 16:08 +// @Author : 黑白配 +// @File : main.go +// @PackageName:WXPay +// @Description:微信支付 + +package WXPay + +import ( + "git.ouxuan.net/3136352472/wxpay/src/V2" + "git.ouxuan.net/3136352472/wxpay/src/V3" + "git.ouxuan.net/3136352472/wxpay/src/config" + "git.ouxuan.net/3136352472/wxpay/src/entity" +) + +type WXPayApi struct { + V2 *V2.WxPay // V2接口 + V3 *V3.API // V3接口 + Config *entity.PayConfig // 配置 +} + +func Init(params entity.PayConfig) (api WXPayApi) { + + api.V2 = V2.Init(params) + + api.V3 = V3.Init(&config.V3{ + MchID: params.MchID, + ClientKeyPath: params.APIClientPath.Key, + SerialNo: params.SerialNo, + }) + api.Config = ¶ms + + return +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..aa230ed --- /dev/null +++ b/main_test.go @@ -0,0 +1,38 @@ +// @Time : 2020/7/9 17:07 +// @Author : 黑白配 +// @File : main_test.go +// @PackageName:WXPay +// @Description: + +package WXPay + +import ( + "git.ouxuan.net/3136352472/wxpay/global" + "git.ouxuan.net/3136352472/wxpay/src/V2" + "git.ouxuan.net/3136352472/wxpay/src/V3/smartGuide" + "testing" +) + +func TestInit(t *testing.T) { + api := Init(global.V2) + if data, err := api.V3.SmartGuide.Query(smartGuide.Query{StoreID: 20774227}); err == nil { + t.Log(data) + } else { + t.Error(err) + } + + if data, err := api.V2.WxAppPay(V2.UnifiedOrder{ + Attach: "支付测试", + OutTradeNo: "11111111111115 ", + TotalFee: 1, + SpbillCreateIP: "127.0.0.1", + OpenID: "owJNp5PDj8lja9S3m2l2M_jt3aHY", + Receipt: "Y", + Body: "测试", + TradeType: "JSAPI", + }); err == nil { + t.Log(data) + } else { + t.Error(err) + } +} diff --git a/src/V2/entity.go b/src/V2/entity.go new file mode 100644 index 0000000..81be88e --- /dev/null +++ b/src/V2/entity.go @@ -0,0 +1,222 @@ +package V2 + +import ( + "errors" +) + +// APIClientPath 微信支付API证书 +type APIClientPath struct { + Cert string // 证书路径 + Key string // 证书密钥路径 + Root string // 根证书路径 +} + +// UnifiedOrder 统一下单参数 +type UnifiedOrder struct { + DeviceInfo string `json:"device_info,omitempty"` // 自定义参数,可以为终端设备号(门店号或收银设备ID) + Body string `json:"body,omitempty"` // 商品简单描述,该字段请按照规范传递 + Detail *UnifiedOrderDetail `json:"detail,omitempty"` // 商品详情 + Attach string `json:"attach,omitempty"` // 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 + OutTradeNo string `json:"out_trade_no,omitempty"` // 商户系统内部订单号 + FeeType string `json:"fee_type,omitempty"` // 币种,默认人民币:CNY + TotalFee int `json:"total_fee,omitempty"` // 订单总金额,单位为分 + SpbillCreateIP string `json:"spbill_create_ip,omitempty"` // 支持IPV4和IPV6两种格式的IP地址。用户的客户端IP + TimeStart string `json:"time_start,omitempty"` // 订单生成时间 + TimeExpire string `json:"time_expire,omitempty"` // 订单失效时间 + GoodsTag string `json:"goods_tag,omitempty"` // 订单优惠标记 + NotifyURL string `json:"notify_url,omitempty"` // 支付结果通知的回调地址 + TradeType string `json:"trade_type,omitempty"` // JSAPI/NATIVE/APP + ProductID string `json:"product_id,omitempty"` // 商品ID trade_type=NATIVE时,此参数必传。 + LimitPay string `json:"limit_pay,omitempty"` // no_credit--可限制用户不能使用信用卡支付 + OpenID string `json:"openid,omitempty"` // 微信用户标识 trade_type=JSAPI时,此参数必传 + SubOpenid string `json:"sub_openid,omitempty"` // 用户在子商户appid下的唯一标识。openid和sub_openid可以选传其中之一,如果选择传sub_openid,则必须传sub_appid + Receipt string `json:"receipt,omitempty"` // Y/N 是否开启电子发票入口 + SceneInfo *SceneInfo `json:"scene_info,omitempty"` // 场景信息 + ProfitSharing string `json:"profit_sharing,omitempty"` // 是否分账 Y/N +} + +// Micropay 付款码支付 +type Micropay struct { + DeviceInfo string `json:"device_info,omitempty"` // 自定义参数,可以为终端设备号(门店号或收银设备ID) + Body string `json:"body,omitempty"` // 商品简单描述,该字段请按照规范传递 + Detail *UnifiedOrderDetail `json:"detail,omitempty"` // 商品详情 + Attach string `json:"attach,omitempty"` // 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 + OutTradeNo string `json:"out_trade_no,omitempty"` // 商户系统内部订单号 + FeeType string `json:"fee_type,omitempty"` // 币种,默认人民币:CNY + TotalFee int `json:"total_fee,omitempty"` // 订单总金额,单位为分 + SpbillCreateIP string `json:"spbill_create_ip,omitempty"` // 支持IPV4和IPV6两种格式的IP地址。用户的客户端IP + TimeStart string `json:"time_start,omitempty"` // 订单生成时间 + TimeExpire string `json:"time_expire,omitempty"` // 订单失效时间 + GoodsTag string `json:"goods_tag,omitempty"` // 订单优惠标记 + LimitPay string `json:"limit_pay,omitempty"` // no_credit--可限制用户不能使用信用卡支付 + SubOpenid string `json:"sub_openid,omitempty"` // 用户在子商户appid下的唯一标识。openid和sub_openid可以选传其中之一,如果选择传sub_openid,则必须传sub_appid + Receipt string `json:"receipt,omitempty"` // Y/N 是否开启电子发票入口 + SceneInfo *SceneInfo `json:"scene_info,omitempty"` // 场景信息 + ProfitSharing string `json:"profit_sharing,omitempty"` // 是否分账 Y/N + AuthCode string `json:"auth_code,omitempty"` // 扫码支付付款码,设备读取用户微信中的条码或者二维码信息 +} + +// UnifiedOrderDetail 商品详情 +type UnifiedOrderDetail struct { + CostPrice int `json:"cost_price,omitempty"` // 订单原价 + ReceiptID string `json:"receipt_id,omitempty"` // 小票ID + GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"` // 商品详情 +} + +// GoodsDetail 商品详情 +type GoodsDetail struct { + GoodsID string `json:"goods_id,omitempty"` // 商品ID + WxpayGoodsID string `json:"wxpay_goods_id,omitempty"` // 微信支付定义的统一商品编号(没有可不传) + GoodsName string `json:"goods_name,omitempty"` // 商品的实际名称 + Quantity int `json:"quantity,omitempty"` // 用户购买的数量 + Price int `json:"price,omitempty"` // 单位为:分。如果商户有优惠,需传输商户优惠后的单价 +} + +// SceneInfo 场景信息 +type SceneInfo struct { + ID string `json:"id,omitempty"` // 门店编号,由商户自定义 + Name string `json:"name,omitempty"` // 门店名称 ,由商户自定义 + AreaCode string `json:"area_code,omitempty"` // 门店所在地行政区划码 + Address string `json:"address,omitempty"` // 门店详细地址 ,由商户自定义 + H5Info *H5Info `json:"h5_info,omitempty"` //h5支付场景信息 +} + +// H5Info h5支付场景信息 +type H5Info struct { + Type string `json:"type,omitempty"` // 场景类型 + AppName string `json:"app_name,omitempty"` // 应用名 + PackageName string `json:"package_name,omitempty"` // 包名 + BundleID string `json:"bundle_id,omitempty"` // bundle_id + WapURL string `json:"wap_url,omitempty"` // WAP网站URL地址 + WapName string `json:"wap_name,omitempty"` // WAP网站名 +} + +// ResultCheck 返回结果检查接口 +type ResultCheck interface { + ResultCheck() error +} + +// PublicResponse 通用返回 +type PublicResponse struct { + ReturnCode string `xml:"return_code,omitempty" json:"return_code,omitempty"` // 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 + ReturnMsg string `xml:"return_msg,omitempty" json:"return_msg,omitempty"` // 当return_code为FAIL时返回信息为错误原因 + ResultCode string `xml:"result_code,omitempty" json:"result_code,omitempty"` // SUCCESS/FAIL + ResultMsg string `xml:"result_msg,omitempty" json:"result_msg,omitempty"` // 对于业务执行的详细描述 + ErrCode string `xml:"err_code,omitempty" json:"err_code,omitempty"` // 当result_code为FAIL时返回错误代码 + ErrCodeDes string `xml:"err_code_des,omitempty" json:"err_code_des,omitempty"` // 当result_code为FAIL时返回错误描述 + ErrorMsg string `xml:"error_msg,omitempty" json:"error_msg,omitempty"` // 当result_code为FAIL时返回错误描述 +} + +// ResultCheck 检查是否返回成功 +func (m PublicResponse) ResultCheck() error { + //s ,_ :=json.Marshal(m) + //log.Println(string(s)) + if m.ReturnCode == "FAIL" { + if m.ReturnMsg != "" { + return errors.New(m.ReturnMsg) + } + if m.ResultMsg != "" { + return errors.New(m.ResultMsg) + } + return errors.New(m.ErrorMsg) + } else if m.ResultCode == "FAIL" { + return errors.New(m.ErrCodeDes) + } + return nil +} + +// CDATA xml +type CDATA struct { + Text string `xml:",cdata"` +} + +// ReverseOrder 撤销订单 +type ReverseOrder struct { + TransactionID string `json:"transaction_id,omitempty"` // 微信支付ID 2选1 + OutTradeNo string `json:"out_trade_no,omitempty"` // 商户订单ID 2选1 +} + +// OrderQuery 查询订单参数 +type OrderQuery struct { + TransactionID string `json:"transaction_id,omitempty"` // 微信支付ID + OutTradeNo string `json:"out_trade_no,omitempty"` // 商户订单ID +} + +// Refund 退款参数 +type Refund struct { + TransactionID string `json:"transaction_id,omitempty"` // 微信支付ID + OutTradeNo string `json:"out_trade_no,omitempty"` // 商户订单ID + OutRefundNo string `json:"out_refund_no,omitempty"` // 商户系统内部的退款单号 + TotalFee int `json:"total_fee,omitempty"` // 订单总金额,单位为分 + RefundFee int `json:"refund_fee,omitempty"` // 退款总金额,订单总金额 + RefundFeeType string `json:"refund_fee_type,omitempty"` // 退款货币类型,需与支付一致,或者不填。 + RefundDesc string `json:"refund_desc,omitempty"` // 若商户传入,会在下发给用户的退款消息中体现退款原因 + RefundAccount string `json:"refund_account,omitempty"` // 退款资金来源 仅针对老资金流商户使用 +} + +// RefundQuery 退款查询参数 +type RefundQuery struct { + TransactionID string `json:"transaction_id,omitempty"` // 微信支付ID 四选一 + OutTradeNo string `json:"out_trade_no,omitempty"` // 商户订单ID 四选一 + OutRefundNo string `json:"out_refund_no,omitempty"` // 商户系统内部的退款单号 四选一 + RefundID string `json:"refund_id,omitempty"` // 微信退款单号 四选一 + Offset int `json:"offset,omitempty"` // 偏移量,当部分退款次数超过10次时可使用,表示返回的查询结果从这个偏移量开始取记录 +} + +// Receiver 分账接收方 +type Receiver struct { + Type string `json:"type,omitempty"` // 账号类型 + Account string `json:"account,omitempty"` // 接收方账号 + Name string `json:"name,omitempty"` // 商户全称 + CustomRelation string `json:"custom_relation,omitempty"` // 子商户与接收方具体的关系,本字段最多10个字 + RelationType string `json:"relation_type,omitempty"` // 分账关系 + Amount int `json:"amount,omitempty"` // 分账金额 + Description string `json:"description,omitempty"` // 分账描述 +} + +// ProfitSharingFinish 分账完成参数 +type ProfitSharingFinish struct { + TransactionID string `json:"transaction_id,omitempty"` // 微信支付ID + OutOrderNo string `json:"out_order_no,omitempty"` // 商户订单ID + Description string `json:"description,omitempty"` // 分账完结的原因描述 +} + +// ProfitSharing 单次分账参数 +type ProfitSharing struct { + TransactionID string `json:"transaction_id,omitempty"` // 微信支付ID + OutOrderNo string `json:"out_order_no,omitempty"` // 商户订单ID + Receivers []Receiver `json:"receivers,omitempty"` // 接收方列表 +} + +// ProfitSharingQuery 分账结果查询参数 +type ProfitSharingQuery struct { + TransactionID string `json:"transaction_id,omitempty"` // 微信支付ID + OutOrderNo string `json:"out_order_no,omitempty"` // 商户订单ID +} + +// ProfitSharingReturn 分账回退参数 +type ProfitSharingReturn struct { + OrderID string `json:"order_id,omitempty"` // 原发起分账请求时,微信返回的微信分账单号,与商户分账单号一一对应 2选1 + OutOrderNo string `json:"out_order_no,omitempty"` // 原发起分账请求时使用的商户后台系统的分账单号 2选1 + OutReturnNo string `json:"out_return_no,omitempty"` // 商户在自己后台生成的一个新的回退单号 + ReturnAccount string `json:"return_account,omitempty"` // 回退方类型是MERCHANT_ID时,填写商户ID + ReturnAmount int `json:"return_amount,omitempty"` // 回退金额 + Description string `json:"description,omitempty"` // 分账回退的原因描述 +} + +// ProfitSharingReturnQuery 分账回退查询参数 +type ProfitSharingReturnQuery struct { + OrderID string `json:"order_id,omitempty"` // 原发起分账请求时,微信返回的微信分账单号,与商户分账单号一一对应 2选1 + OutOrderNo string `json:"out_order_no,omitempty"` // 原发起分账请求时使用的商户后台系统的分账单号 2选1 + OutReturnNo string `json:"out_return_no,omitempty"` // 商户系统内部的回退单号 +} + +// Transfers 付款到零钱 +type Transfers struct { + PartnerTradeNo string `json:"partner_trade_no"` // 商户订单号 + OpenID string `json:"openid"` // OPENID + CheckName string `json:"check_name"` // NO_CHECK:不校验真实姓名 FORCE_CHECK:强校验真实姓名 + ReUserName string `json:"re_user_name"` // 收款用户真实姓名。如果check_name设置为FORCE_CHECK,则必填用户真实姓名。如需电子回单,需要传入收款用户姓名 + Amount int `json:"amount"` // 企业付款金额,单位为分 + Desc string `json:"desc"` // 企业付款备注,必填。注意:备注中的敏感词会被转成字符* +} diff --git a/src/V2/index.go b/src/V2/index.go new file mode 100644 index 0000000..54e0457 --- /dev/null +++ b/src/V2/index.go @@ -0,0 +1,13 @@ +// @Time : 2020/7/9 16:03 +// @Author : 黑白配 +// @File : index.go +// @PackageName:V2 +// @Description:V2版本接口 + +package V2 + +import "git.ouxuan.net/3136352472/wxpay/src/entity" + +func Init(config entity.PayConfig) *WxPay { + return &WxPay{config: config} +} diff --git a/src/V2/pay.go b/src/V2/pay.go new file mode 100644 index 0000000..cd05a4d --- /dev/null +++ b/src/V2/pay.go @@ -0,0 +1,303 @@ +package V2 + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "encoding/xml" + "errors" + "io" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" + + "git.ouxuan.net/3136352472/wxpay/src/entity" + "git.ouxuan.net/3136352472/wxpay/utils" +) + +// WxPay 微信支付 +type WxPay struct { + config entity.PayConfig +} + +// 网络请求 +func (c WxPay) request(url string, body io.Reader, cert bool) (map[string]string, error) { + + var client http.Client + + if cert { + if err := c.checkClient(); err != nil { + return nil, err + } + // 微信提供的API证书,证书和证书密钥 .pem格式 + certs, _ := tls.LoadX509KeyPair(c.config.APIClientPath.Cert, c.config.APIClientPath.Key) + // 微信支付HTTPS服务器证书的根证书 .pem格式 + rootCa, _ := ioutil.ReadFile(c.config.APIClientPath.Root) + + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(rootCa) + + client = http.Client{Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{certs}, + InsecureSkipVerify: true, + }, + }} + } + + if err := c.checkConfig(); err != nil { + return nil, err + } + + resp, err := client.Post(url, "", body) + if err == nil { + defer resp.Body.Close() + b, _ := ioutil.ReadAll(resp.Body) + var result PublicResponse + _ = xml.Unmarshal(b, &result) + err := result.ResultCheck() + if err == nil { + return utils.XML2MAP(b), nil + } + return nil, err + } + return nil, err +} + +// 公共参数 +func (c WxPay) publicParams() (m map[string]interface{}) { + m = make(map[string]interface{}) + m["appid"] = c.config.AppID + m["mch_id"] = c.config.MchID + m["nonce_str"] = utils.RandomStr() + if c.config.SubMchID != "" { + m["sub_mch_id"] = c.config.SubMchID + } + if c.config.SubAppID != "" { + m["sub_appid"] = c.config.SubAppID + } + m["sign_type"] = "HMAC-SHA256" + return +} + +// 检查支付配置 +func (c WxPay) checkConfig() (err error) { + + if c.config.AppID == "" { + err = errors.New("AppID不能为空") + } else if c.config.MchID == "" { + err = errors.New("MchID不能为空") + } else if c.config.Secret == "" { + err = errors.New("Secret不能为空") + } else { + err = nil + } + return +} + +// 检查支付证书 +func (c WxPay) checkClient() (err error) { + if c.config.APIClientPath.Cert == "" { + err = errors.New("APIClientPath.Cert 不能为空") + } else if c.config.APIClientPath.Key == "" { + err = errors.New("APIClientPath.Key 不能为空") + } else if c.config.APIClientPath.Root == "" { + err = errors.New("APIClientPath.Key 不能为空") + } else { + err = nil + } + return +} + +// UnifiedOrder 统一下单接口 +func (c WxPay) UnifiedOrder(params UnifiedOrder) (map[string]string, error) { + + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + + m["notify_url"] = c.config.PayNotify + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + + return c.request("https://api.mch.weixin.qq.com/pay/unifiedorder", strings.NewReader(utils.MAP2XML(m)), false) +} + +// WxAppPay 小程序下单接口 +func (c WxPay) WxAppPay(params UnifiedOrder) (map[string]interface{}, error) { + params.TradeType = "JSAPI" + m, err := c.UnifiedOrder(params) + if err == nil { + result := make(map[string]interface{}) + result["appId"] = m["appid"] + result["nonceStr"] = m["nonce_str"] + result["package"] = "prepay_id=" + m["prepay_id"] + result["timeStamp"] = strconv.FormatInt(time.Now().Unix(), 10) + result["signType"] = "HMAC-SHA256" + result["paySign"] = utils.SignHMACSHA256(result, c.config.Secret) + return result, err + } + return nil, err +} + +// WxAppAppPay APP下单接口 +func (c WxPay) WxAppAppPay(params UnifiedOrder) (map[string]interface{}, error) { + params.TradeType = "APP" + m, err := c.UnifiedOrder(params) + if err == nil { + result := make(map[string]interface{}) + result["appid"] = m["appid"] + result["noncestr"] = m["nonce_str"] + result["package"] = "Sign=WXPay" + if params.ProfitSharing != "" { + result["partnerid"] = m["sub_mch_id"] + } else { + result["partnerid"] = m["mch_id"] + } + result["prepayid"] = m["prepay_id"] + result["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10) + result["paySign"] = utils.SignHMACSHA256(result, c.config.Secret) + return result, err + } + return nil, err +} + +// WxH5Pay H5下单接口 SceneInfo 场景信息需使用json字符串 +func (c WxPay) WxH5Pay(params UnifiedOrder) (map[string]interface{}, error) { + params.TradeType = "MWEB" + m, err := c.UnifiedOrder(params) + if err == nil { + result := make(map[string]interface{}) + result["mweburl"] = m["mweb_url"] + return result, err + } + return nil, err +} + +// Micropay 付款码支付 +func (c WxPay) Micropay(params Micropay) (map[string]string, error) { + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/pay/micropay", strings.NewReader(utils.MAP2XML(m)), false) +} + +// CloseOrder 关闭订单 +func (c WxPay) CloseOrder(outTradeNo string) (map[string]string, error) { + m := c.publicParams() + m["out_trade_no"] = outTradeNo + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/pay/closeorder", strings.NewReader(utils.MAP2XML(m)), false) +} + +// ReverseOrder 撤销订单 +func (c WxPay) ReverseOrder(params ReverseOrder) (map[string]string, error) { + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/secapi/pay/reverse", strings.NewReader(utils.MAP2XML(m)), true) +} + +// OrderQuery 查询订单 +func (c WxPay) OrderQuery(params OrderQuery) (map[string]string, error) { + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/pay/orderquery", strings.NewReader(utils.MAP2XML(m)), false) +} + +// Refund 申请退款 +func (c WxPay) Refund(params Refund) (map[string]string, error) { + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + m["notify_url"] = c.config.RefundNotify + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/secapi/pay/refund", strings.NewReader(utils.MAP2XML(m)), true) +} + +// RefundQuery 查询退款 +func (c WxPay) RefundQuery(params RefundQuery) (map[string]string, error) { + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/pay/refundquery", strings.NewReader(utils.MAP2XML(m)), false) +} + +// ProfitSharingAddReceiver 添加分账接受方 +func (c WxPay) ProfitSharingAddReceiver(params Receiver) (map[string]string, error) { + m := c.publicParams() + b, _ := json.Marshal(params) + m["receiver"] = string(b) + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/pay/profitsharingaddreceiver", strings.NewReader(utils.MAP2XML(m)), false) +} + +// ProfitSharingRemoveReceiver 删除分账接收方 +func (c WxPay) ProfitSharingRemoveReceiver(params Receiver) (map[string]string, error) { + m := c.publicParams() + b, _ := json.Marshal(params) + m["receiver"] = string(b) + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/pay/profitsharingremovereceiver", strings.NewReader(utils.MAP2XML(m)), false) +} + +// ProfitSharingFinish 完成分账 +func (c WxPay) ProfitSharingFinish(params ProfitSharingFinish) (map[string]string, error) { + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + delete(m, "sub_appid") + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/secapi/pay/profitsharingfinish", strings.NewReader(utils.MAP2XML(m)), true) +} + +// ProfitSharing 开始分账 默认为单次分账 option=multi为多次分账 +func (c WxPay) ProfitSharing(params ProfitSharing, option string) (map[string]string, error) { + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + b, _ := json.Marshal(params.Receivers) + m["receivers"] = string(b) + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + if option == "multi" { + return c.request("https://api.mch.weixin.qq.com/secapi/pay/multiprofitsharing", strings.NewReader(utils.MAP2XML(m)), true) + } + return c.request("https://api.mch.weixin.qq.com/secapi/pay/profitsharing", strings.NewReader(utils.MAP2XML(m)), true) +} + +// ProfitSharingQuery 查询分账结果 +func (c WxPay) ProfitSharingQuery(params ProfitSharingQuery) (map[string]string, error) { + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + delete(m, "appid") + delete(m, "sub_appid") + + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/pay/profitsharingquery", strings.NewReader(utils.MAP2XML(m)), false) +} + +// ProfitSharingReturn 分账回退 +func (c WxPay) ProfitSharingReturn(params ProfitSharingReturn) (map[string]string, error) { + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + m["return_account_type"] = "MERCHANT_ID" + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/secapi/pay/profitsharingreturn", strings.NewReader(utils.MAP2XML(m)), true) +} + +// ProfitSharingReturnQuery 分账回退结果查询 +func (c WxPay) ProfitSharingReturnQuery(params ProfitSharingReturnQuery) (map[string]string, error) { + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + delete(m, "sub_appid") + m["sign"] = utils.SignHMACSHA256(m, c.config.Secret) + return c.request("https://api.mch.weixin.qq.com/pay/profitsharingreturnquery", strings.NewReader(utils.MAP2XML(m)), false) +} + +// Transfers 企业付款到零钱 +func (c WxPay) Transfers(params Transfers) (map[string]string, error) { + + m := utils.MAPMerge(utils.Struct2Map(params), c.publicParams()) + delete(m, "appid") + delete(m, "mch_id") + delete(m, "sign_type") + + m["mchid"] = c.config.MchID + m["mch_appid"] = c.config.AppID + m["sign"] = utils.SignMD5(m, c.config.Secret) + + return c.request("https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers", strings.NewReader(utils.MAP2XML(m)), true) +} + +// NotifyFormat 通知数据解析 +func NotifyFormat(data string) map[string]string { + return utils.XML2MAP([]byte(data)) +} diff --git a/src/V2/pay_test.go b/src/V2/pay_test.go new file mode 100644 index 0000000..b258ca2 --- /dev/null +++ b/src/V2/pay_test.go @@ -0,0 +1,240 @@ +package V2 + +import ( + "git.ouxuan.net/3136352472/wxpay/global" + "log" + "testing" +) + +var pay = WxPay{ + global.V2, +} + +func TestWxPay_UnifiedOrder(t *testing.T) { + if result, err := pay.UnifiedOrder(UnifiedOrder{ + Attach: "支付测试", + OutTradeNo: "11111111111114", + TotalFee: 1, + SpbillCreateIP: "127.0.0.1", + OpenID: "owJNp5PDj8lja9S3m2l2M_jt3aHY", + Receipt: "Y", + Body: "测试", + TradeType: "JSAPI", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_WxAppPay(t *testing.T) { + if result, err := pay.WxAppPay(UnifiedOrder{ + Attach: "支付测试", + OutTradeNo: "11111111111115 ", + TotalFee: 1, + SpbillCreateIP: "127.0.0.1", + OpenID: "owJNp5PDj8lja9S3m2l2M_jt3aHY", + Receipt: "Y", + Body: "测试", + TradeType: "JSAPI", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_Micropay(t *testing.T) { + if result, err := pay.Micropay(Micropay{ + Attach: "支付测试", + OutTradeNo: "11111111111115", + TotalFee: 1, + SpbillCreateIP: "127.0.0.1", + Receipt: "Y", + Body: "测试", + AuthCode: "12312312312", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_CloseOrder(t *testing.T) { + if result, err := pay.CloseOrder("11111111111112"); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_ReverseOrder(t *testing.T) { + if result, err := pay.ReverseOrder(ReverseOrder{ + OutTradeNo: "11111111111112", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_OrderQuery(t *testing.T) { + if result, err := pay.OrderQuery(OrderQuery{ + OutTradeNo: "674BB66E408A6931788347BF25E9BCAA", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_Refund(t *testing.T) { + if result, err := pay.Refund(Refund{ + OutTradeNo: "BC9AA9B9E43C13E7932CB3B181468A4F", + TotalFee: 990, + RefundFee: 990, + OutRefundNo: "BC9AA9B9E43C13E7932CB3B181468A4F", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_RefundQuery(t *testing.T) { + if result, err := pay.RefundQuery(RefundQuery{ + OutTradeNo: "11111111111113", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_ProfitSharingAddReceiver(t *testing.T) { + if result, err := pay.ProfitSharingAddReceiver(Receiver{ + Type: "PERSONAL_OPENID", + Account: "owJNp5M6Trxg5qBjh8KTPnTm65Sg", + RelationType: "DISTRIBUTOR", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_ProfitSharingRemoveReceiver(t *testing.T) { + if result, err := pay.ProfitSharingRemoveReceiver(Receiver{ + Type: "PERSONAL_OPENID", + Account: "owJNp5PDj8lja9S3m2l2M_jt3aHY", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_ProfitSharingFinish(t *testing.T) { + if result, err := pay.ProfitSharingFinish(ProfitSharingFinish{ + TransactionID: "11111", + OutOrderNo: "111111", + Description: "分账完成", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_ProfitSharing(t *testing.T) { + if result, err := pay.ProfitSharing(ProfitSharing{ + TransactionID: "11111", + OutOrderNo: "111111", + Receivers: []Receiver{{Type: "PERSONAL_OPENID", Account: "owJNp5PDj8lja9S3m2l2M_jt3aHY", Amount: 1, Description: "个人分成"}}, + }, "multi"); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_ProfitSharingQuery(t *testing.T) { + if result, err := pay.ProfitSharingQuery(ProfitSharingQuery{ + TransactionID: "11111", + OutOrderNo: "111111", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_ProfitSharingReturn(t *testing.T) { + if result, err := pay.ProfitSharingReturn(ProfitSharingReturn{ + OutOrderNo: "1111111", + OutReturnNo: "1111112", + ReturnAccount: "12312312", + ReturnAmount: 100, + Description: "回退", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestWxPay_ProfitSharingReturnQuery(t *testing.T) { + if result, err := pay.ProfitSharingReturnQuery(ProfitSharingReturnQuery{ + OutOrderNo: "1111111", + OutReturnNo: "1111112", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} + +func TestNotifyDataFormat(t *testing.T) { + x := ` + + + + + + + + + + + + + + 1 + + + + + + + + ` + m := NotifyFormat(x) + + log.Println(m) + +} + +func TestWxPay_Transfers(t *testing.T) { + + if result, err := pay.Transfers(Transfers{ + PartnerTradeNo: "1", + OpenID: "owJNp5PDj8lja9S3m2l2M_jt3aHY", + CheckName: "NO_CHECK", + Amount: 100, + Desc: "付款测试", + }); err == nil { + log.Println(result) + } else { + t.Error(err) + } +} diff --git a/src/V3/index.go b/src/V3/index.go new file mode 100644 index 0000000..c09c720 --- /dev/null +++ b/src/V3/index.go @@ -0,0 +1,20 @@ +// @Time : 2020/7/8 16:04 +// @Author : 黑白配 +// @File : index.go +// @PackageName:V3 +// @Description:V3版本接口 + +package V3 + +import ( + "git.ouxuan.net/3136352472/wxpay/src/V3/smartGuide" + "git.ouxuan.net/3136352472/wxpay/src/config" +) + +type API struct { + SmartGuide *smartGuide.SmartGuide +} + +func Init(params *config.V3) *API { + return &API{SmartGuide: smartGuide.New(params)} +} diff --git a/src/V3/smartGuide/entity.go b/src/V3/smartGuide/entity.go new file mode 100644 index 0000000..5e163d3 --- /dev/null +++ b/src/V3/smartGuide/entity.go @@ -0,0 +1,64 @@ +// @Time : 2020/7/8 14:38 +// @Author : 黑白配 +// @File : entity.go +// @PackageName:smartGuide +// @Description:entity + +package smartGuide + +import "git.ouxuan.net/3136352472/wxpay/src/config" + +// Register 注册服务人员参数 +type Register struct { + SubMchID string `json:"sub_mchid,omitempty"` // 子商户ID + CorpID string `json:"corpid,omitempty"` // 企业ID + StoreID int `json:"store_id,omitempty"` // 门店ID + Name string `json:"name,omitempty"` // 员工姓名 + UserId string `json:"userid,omitempty"` // 企业微信的员工ID + Mobile string `json:"mobile,omitempty"` // 手机号码 + QRCode string `json:"qr_code,omitempty"` // 二维码 + Avatar string `json:"avatar,omitempty"` // 头像 +} + +// RegisterResult 注册服务人员返回数据 +type RegisterResult struct { + GuideID string `json:"guide_id,omitempty"` // 服务人员ID + config.Result +} + +// Assign 服务人员分配参数 +type Assign struct { + GuideID string `json:"guide_id,omitempty"` // 服务人员ID + SubMchID string `json:"sub_mchid,omitempty"` // 子商户ID + OutTradeNo string `json:"out_trade_no,omitempty"` // 商户订单号 +} + +// Query 查询参数 +type Query struct { + SubMchID string `json:"sub_mchid,omitempty"` // 子商户ID + StoreID int `json:"store_id,omitempty"` // 门店ID + UserId string `json:"userid,omitempty"` // 企业微信的员工ID + Mobile string `json:"mobile,omitempty"` // 手机号码 + WorkID string `json:"work_id,omitempty"` // 工号 + Limit int `json:"limit,omitempty"` // 最大资源条数 + Offset int `json:"offset,omitempty"` // 请求资源起始位置 +} + +// UserInfo 服务人员信息 +type UserInfo struct { + GuideID string `json:"guide_id,omitempty"` // 服务人员ID + StoreID int `json:"store_id,omitempty"` // 门店ID + Name string `json:"name,omitempty"` // 员工姓名 + UserId string `json:"userid,omitempty"` // 企业微信的员工ID + Mobile string `json:"mobile,omitempty"` // 手机号码 + WorkID string `json:"work_id,omitempty"` // 工号 +} + +// QueryResult 查询结果 +type QueryResult struct { + Data []UserInfo `json:"data,omitempty"` // 服务人员列表 + TotalCount int `json:"total_count,omitempty"` // 服务人员数量 + Limit int `json:"limit,omitempty"` // 最大资源条数 + Offset int `json:"offset,omitempty"` // 请求资源起始位置 + config.Result +} diff --git a/src/V3/smartGuide/index.go b/src/V3/smartGuide/index.go new file mode 100644 index 0000000..43dfcec --- /dev/null +++ b/src/V3/smartGuide/index.go @@ -0,0 +1,47 @@ +// @Time : 2020/7/8 14:26 +// @Author : 黑白配 +// @File : index.go +// @PackageName:smartGuide +// @Description: 支付即服务 + +package smartGuide + +import ( + "git.ouxuan.net/3136352472/wxpay/src/config" + "git.ouxuan.net/3136352472/wxpay/utils" +) + +func New(config *config.V3) *SmartGuide { + return &SmartGuide{config: config} +} + +type SmartGuide struct { + config *config.V3 +} + +// Register 服务人员注册 +// +// https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/smartguide/chapter3_1.shtml +func (m SmartGuide) Register(register Register) (result RegisterResult, err error) { + err = m.config.Request("/v3/smartguide/guides", register, &result) + return +} + +// Assign 服务人员分配 +// +// https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/smartguide/chapter3_1.shtml +func (m SmartGuide) Assign(assign Assign) (result interface{}, err error) { + id := assign.GuideID + assign.GuideID = "" + err = m.config.Request("/v3/smartguide/guides/"+id+"/assign", assign, nil) + return +} + +// Query 服务人员查询 +// +// https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/smartguide/chapter3_3.shtml +func (m SmartGuide) Query(query Query) (result QueryResult, err error) { + params := utils.SortKey(utils.Struct2Map(query)) + err = m.config.Request("/v3/smartguide/guides?"+params, nil, &result) + return +} diff --git a/src/V3/smartGuide/index_test.go b/src/V3/smartGuide/index_test.go new file mode 100644 index 0000000..486e913 --- /dev/null +++ b/src/V3/smartGuide/index_test.go @@ -0,0 +1,49 @@ +// @Time : 2020/7/8 14:49 +// @Author : 黑白配 +// @File : index_test.go +// @PackageName:smartGuide +// @Description: + +package smartGuide + +import ( + "git.ouxuan.net/3136352472/wxpay/global" + "testing" +) + +var smartGuide = New(&global.V3) + +func TestSmartGuide_Query(t *testing.T) { + if data, err := smartGuide.Query(Query{StoreID: 20774227}); err == nil { + t.Log(data) + } else { + t.Error(err) + } +} + +func TestSmartGuide_Assign(t *testing.T) { + if data, err := smartGuide.Assign(Assign{ + GuideID: "01007196110000000001", + OutTradeNo: "8319B95CFEDE8F60A77A5BDE4E359E01", + }); err == nil { + t.Log(data) + } else { + t.Error(err) + } +} + +func TestSmartGuide_Register(t *testing.T) { + if data, err := smartGuide.Register(Register{ + CorpID: "123", + StoreID: 20774227, + Name: "test", + UserId: "123", + Mobile: "123", + QRCode: "123", + Avatar: "123", + }); err == nil { + t.Log(data) + } else { + t.Error(err) + } +} diff --git a/src/config/index.go b/src/config/index.go new file mode 100644 index 0000000..ec84c70 --- /dev/null +++ b/src/config/index.go @@ -0,0 +1,119 @@ +// @Time : 2020/7/9 11:14 +// @Author : 黑白配 +// @File : index +// @PackageName:public +// @Description: + +package config + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "net/http" + "time" + + "git.ouxuan.net/3136352472/wxpay/utils" +) + +type V3 struct { + MchID string `json:"mchid"` // 商户ID + ClientKeyPath string `json:"clientKeyPath"` // 证书私钥路径 + SerialNo string `json:"serialNo"` // 证书编号 +} + +func (m V3) Request(api string, body interface{}, out interface{}) error { + + var request *http.Request + client := http.Client{} + + host := "https://api.mch.weixin.qq.com" + if body != nil { + b, _ := json.Marshal(body) + request, _ = http.NewRequest("POST", host+api, bytes.NewReader(b)) + } else { + request, _ = http.NewRequest("GET", host+api, nil) + } + sign, err := m.Sign(api, body) + if err != nil { + return err + } + + request.Header.Add("Accept", "application/json") + request.Header.Add("Content-Type", "application/json") + request.Header.Add("Authorization", sign) + if resp, err := client.Do(request); err == nil { + defer resp.Body.Close() + b, _ := ioutil.ReadAll(resp.Body) + if len(b) > 0 && out != nil { + return json.Unmarshal(b, out) + } else { + return nil + } + } else { + return err + } +} + +// Sign 签名 +// @param method string 请求类型 +// @param url string 请求地址 +// @param body interface 请求数据 +func (m V3) Sign(url string, body interface{}) (string, error) { + var data = "" + var method = "GET" + t := time.Now().Unix() + randomStr := utils.RandomStr() + + if body != nil { + b, _ := json.Marshal(body) + data = string(b) + method = "POST" + } + + str := fmt.Sprintf("%s\n%s\n%d\n%s\n%s\n", method, url, t, randomStr, data) + key, _ := ioutil.ReadFile(m.ClientKeyPath) + + sign, err := m.rsaEncrypt([]byte(str), key) + + return fmt.Sprintf("WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\","+ + "signature=\"%s\",timestamp=\"%d\",serial_no=\"%s\"", m.MchID, randomStr, sign, t, m.SerialNo), err +} + +// 私钥加密 +func (m V3) rsaEncrypt(data, keyBytes []byte) (string, error) { + //解密pem格式的私钥 + block, _ := pem.Decode(keyBytes) + if block == nil { + return "", errors.New("public key error") + } + // 解析私钥 + pubInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return "", err + } + // 类型断言 + pub := pubInterface.(*rsa.PrivateKey) + //加密 + msg := sha256.Sum256(data) + sign, err := rsa.SignPKCS1v15(rand.Reader, pub, crypto.SHA256, msg[:]) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(sign), nil +} + +type Result struct { + Code string `json:"code,omitempty"` // 错误代码 + Message string `json:"message,omitempty"` // 错误信息 +} diff --git a/src/entity/index.go b/src/entity/index.go new file mode 100644 index 0000000..449e667 --- /dev/null +++ b/src/entity/index.go @@ -0,0 +1,33 @@ +// @Time : 2020/7/9 16:51 +// @Author : 黑白配 +// @File : index +// @PackageName:entity +// @Description: + +package entity + +type PayConfig struct { + AppID string // 商户/服务商 AppId(公众号/小程序) + MchID string // 商户/服务商 商户号 + SubAppID string // 子商户公众号ID + SubMchID string // 子商户商户号 + PayNotify string // 支付结果回调地址 + RefundNotify string // 退款结果回调地址 + Secret string // 微信支付密钥 + APIClientPath APIClientPath // API证书路径,使用V3接口必传 + SerialNo string // 证书编号,使用V3接口必传 +} + +// APIClientPath 微信支付API证书 +type APIClientPath struct { + Cert string // 证书路径 + Key string // 私钥证书路径,使用V3接口必传 + Root string // 根证书路径 +} + +// APIClientData 微信支付API证书 +type APIClientData struct { + Cert []byte // 证书路径 + Key []byte // 私钥证书路径,使用V3接口必传 + Root []byte // 根证书路径 +} diff --git a/utils/public.go b/utils/public.go new file mode 100644 index 0000000..c00c02b --- /dev/null +++ b/utils/public.go @@ -0,0 +1,151 @@ +package utils + +import ( + "bytes" + "crypto/hmac" + "crypto/md5" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "encoding/xml" + "fmt" + "sort" + "strconv" + "strings" + "time" +) + +// Struct2Map 结构体转map +func Struct2Map(params interface{}) (m map[string]interface{}) { + m = make(map[string]interface{}) + + if b, err := json.Marshal(params); err == nil { + _ = json.Unmarshal(b, &m) + } + return +} + +// SortKey map排序 +func SortKey(m map[string]interface{}) string { + arr := make([]string, 0) + for k := range m { + arr = append(arr, k) + } + + strArr := make([]string, 0) + + for _, key := range arr { + switch m[key].(type) { + case string: + value := m[key].(string) + if value != "" { + strArr = append(strArr, key+"="+value) + } + case int: + if m[key] != 0 { + value := strconv.Itoa(m[key].(int)) + strArr = append(strArr, key+"="+value) + } + case float64: + if m[key] != 0.00 { + value := strconv.Itoa(int(m[key].(float64))) + strArr = append(strArr, key+"="+value) + } + } + } + + sort.Strings(strArr) + return strings.Join(strArr, "&") +} + +// MAP2XML map转xml +func MAP2XML(m map[string]interface{}) string { + str := "" + for k, v := range m { + switch v.(type) { + case string: + str = str + fmt.Sprintf("<%s>", k, v, k) + case int: + str = str + fmt.Sprintf("<%s>", k, v, k) + case interface{}: + b, _ := json.Marshal(v) + str = str + fmt.Sprintf("<%s>", k, string(b), k) + } + } + return "" + str + "" +} + +// MAPMerge map合并 +func MAPMerge(args ...map[string]interface{}) (m map[string]interface{}) { + m = make(map[string]interface{}) + for _, item := range args { + for k, v := range item { + m[k] = v + } + } + return m +} + +// XML2MAP xml转map +func XML2MAP(b []byte) (m map[string]string) { + + decoder := xml.NewDecoder(bytes.NewReader(b)) + m = make(map[string]string) + tag := "" + for { + token, err := decoder.Token() + + if err != nil { + break + } + switch t := token.(type) { + case xml.StartElement: + if t.Name.Local != "xml" { + tag = t.Name.Local + } else { + tag = "" + } + break + case xml.EndElement: + break + case xml.CharData: + data := strings.TrimSpace(string(t)) + if len(data) != 0 { + m[tag] = data + } + break + } + } + return +} + +// RandomStr 随机字符串 +func RandomStr() string { + return strings.ToUpper(MD5(strconv.FormatInt(time.Now().UnixNano(), 19))) +} + +// MD5 md5加密 +func MD5(str string) string { + hash := md5.Sum([]byte(str)) + md5str := fmt.Sprintf("%x", hash) + return strings.ToUpper(md5str) +} + +// Sign 生成签名 HMAC-SHA256加密 +func SignHMACSHA256(m map[string]interface{}, key string) (sign string) { + sign = HmacSha256(SortKey(m)+"&key="+key, key) + return +} + +// SignMD5 生成签名 MD5加密 +func SignMD5(m map[string]interface{}, key string) (sign string) { + sign = MD5(SortKey(m) + "&key=" + key) + return +} + +// HmacSha256 HMAC-SHA256加密 +func HmacSha256(str string, key string) string { + h := hmac.New(sha256.New, []byte(key)) + h.Write([]byte(str)) + return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) +}