diff --git a/client.go b/client.go index 9517308..49c3aa3 100644 --- a/client.go +++ b/client.go @@ -52,9 +52,27 @@ type WePayer interface { GetPublicKey(p12CertPath string, targetPath string) error //TODO 待验证 企业付款到银行卡 TransferBank(param Param, p12CertPath string, publicKeyPath string) (ResultParam, error) - ////TODO 待验证 查询企业付款到银行卡 + //TODO 待验证 查询企业付款到银行卡 TransferBankQuery(param Param, p12CertPath string) (ResultParam, error) + //分账相关 + //申请单次分账or多次分账 + ProfitSharing(param Param, p12CertPath string, multiTag bool) (ResultParam, error) + //查询申请分账的结果 + QueryProfitSharing(param Param, p12CertPath string) (ResultParam, error) + //添加分账接收方 + AddProfitSharingReceiver(param Param) (ResultParam, error) + //删除分账接收方 + RemoveProfitSharingReceiver(param Param) (ResultParam, error) + //完结分账 + FinishProfitSharing(param Param, p12CertPath string) (ResultParam, error) + //分账回退 + ReturnProfitSharing(param Param, p12CertPath string) (ResultParam, error) + //回退结果查询 + QueryProfitSharingReturn(param Param) (ResultParam, error) + //分账动帐通知(此处无视返回结果的层级关系,对需要的字段直接使用Get方法获取对应的结果) + ProfitSharingNotify(body io.Reader) (ResultParam, error) + //小程序相关 //获取授权access_token GetAccessTokenForMini() (Param, error) //获取小程序接口凭证,使用者自己保存token,过期重新获取 @@ -80,11 +98,12 @@ var ( ) type myPayer struct { - once sync.Once - appId string //appid - mchId string //mchid - secret string //secret用于获取token - apiKey string //用于支付 + once sync.Once + appId string //appid + mchId string //mchid + secret string //secret用于获取token + apiKey string //用于支付 + apiV3Key string //api v3 key } //不向微信发送接口请求report @@ -122,6 +141,12 @@ func WithApiKey(key string) option { } } +func WithApiV3Key(v3Key string) option { + return func(payer *myPayer) { + payer.apiV3Key = v3Key + } +} + func (m *myPayer) checkForPay() error { if m.appId == "" { return errors.New("need appid") diff --git a/pay.go b/pay.go index 9694786..4e58f77 100644 --- a/pay.go +++ b/pay.go @@ -31,7 +31,7 @@ func (m *myPayer) UnifiedOrder(param Param) (ResultParam, error) { //获取交易类型和签名类型 var ( unifiedMustParam = []string{"appid", "mch_id", "nonce_str", "sign", "body", "out_trade_no", "total_fee", "spbill_create_ip", "notify_url", "trade_type"} - unifiedOptionalParam = []string{"device_info", "sign_type", "detail", "attach", "fee_type", "time_start", "time_expire", "goods_tag", "limit_pay", "receipt", "openid", "product_id", "scene_info"} + unifiedOptionalParam = []string{"device_info", "sign_type", "detail", "attach", "fee_type", "time_start", "time_expire", "goods_tag", "limit_pay", "receipt", "openid", "product_id", "scene_info", "profit_sharing"} tradeType string signType = e.SignTypeMD5 //默认MD5签名方式 ) @@ -228,7 +228,7 @@ func (m *myPayer) UnifiedMicro(param Param) (ResultParam, error) { //获取交易类型和签名类型 var ( microMustParam = []string{"appid", "mch_id", "nonce_str", "sign", "body", "out_trade_no", "total_fee", "spbill_create_ip", "auth_code"} - microOptionalParam = []string{"device_info", "sign_type", "detail", "attach", "fee_type", "goods_tag", "limit_pay", "time_start", "time_expire", "receipt", "scene_info"} + microOptionalParam = []string{"device_info", "sign_type", "detail", "attach", "fee_type", "goods_tag", "limit_pay", "time_start", "time_expire", "receipt", "scene_info", "profit_sharing"} signType = e.SignTypeMD5 //默认MD5签名方式 ) diff --git a/profitsharing.go b/profitsharing.go new file mode 100644 index 0000000..2264036 --- /dev/null +++ b/profitsharing.go @@ -0,0 +1,653 @@ +package wechat_sdk + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + + "github.com/pyihe/secret" + "github.com/pyihe/wechat-sdk/pkg/e" + "github.com/pyihe/wechat-sdk/pkg/util" +) + +func (m *myPayer) ProfitSharing(param Param, p12CertPath string, multiTag bool) (ResultParam, error) { + if param == nil { + return nil, e.ErrParams + } + if err := m.checkForPay(); err != nil { + return nil, err + } + + //读取证书 + cert, err := util.P12ToPem(p12CertPath, m.mchId) + if err != nil { + return nil, err + } + + param.Add("appid", m.appId) + param.Add("mch_id", m.mchId) + + var ( + shareMustParam = []string{"appid", "mch_id", "nonce_str", "sign", "transaction_id", "out_order_no", "receivers"} + shareOptionalParam = []string{"sign_type"} + signType = e.SignType256 + ) + + if t, ok := param["sign_type"]; ok { + if t.(string) != e.SignType256 { + return nil, errors.New("only support HMAC-SHA256") + } + } + + for _, v := range shareMustParam { + if v == "sign" { + continue + } + if _, ok := param[v]; !ok { + return nil, errors.New("need param: " + v) + } + } + for key := range param { + if !util.HaveInArray(shareMustParam, key) && !util.HaveInArray(shareOptionalParam, key) { + return nil, errors.New("no need param: " + key) + } + } + + sign := param.Sign(signType) + param.Add("sign", sign) + + reader, err := param.MarshalXML() + if err != nil { + return nil, err + } + var url = "https://api.mch.weixin.qq.com/secapi/pay/profitsharing" + if multiTag { + url = "https://api.mch.weixin.qq.com/secapi/pay/multiprofitsharing" + } + var request = &postRequest{ + Body: reader, + Url: url, + ContentType: e.PostContentType, + } + response, err := postToWxWithCert(request, cert) + if err != nil { + return nil, err + } + defer response.Body.Close() + result := ParseXMLReader(response.Body) + if returnCode, _ := result.GetString("return_code"); returnCode != "SUCCESS" { + returnMsg, _ := result.GetString("return_msg") + return nil, errors.New(returnMsg) + } + if resultCode, _ := result.GetString("result_code"); resultCode != "SUCCESS" { + errDes, _ := result.GetString("err_code_des") + return nil, errors.New(errDes) + } + sign = result.Sign(signType) + if resultSign, _ := result.GetString("sign"); resultSign != sign { + return nil, e.ErrCheckSign + } + return result, nil +} + +func (m *myPayer) QueryProfitSharing(param Param, p12CertPath string) (ResultParam, error) { + if param == nil { + return nil, e.ErrParams + } + if err := m.checkForPay(); err != nil { + return nil, err + } + + param.Add("mch_id", m.mchId) + + var ( + mustParam = []string{"mch_id", "nonce_str", "sign", "transaction_id", "out_order_no"} + optionalParam = []string{"sign_type"} + signType = e.SignType256 + ) + if t, ok := param["sign_type"]; ok { + if t.(string) != e.SignType256 { + return nil, errors.New("only support HMAC-SHA256") + } + } + for _, v := range mustParam { + if v == "sign" { + continue + } + if _, ok := param[v]; !ok { + return nil, errors.New("need param: " + v) + } + } + + for key := range param { + if !util.HaveInArray(mustParam, key) && !util.HaveInArray(optionalParam, key) { + return nil, errors.New("no need param: " + key) + } + } + + sign := param.Sign(signType) + param.Add("sign", sign) + + reader, err := param.MarshalXML() + if err != nil { + return nil, err + } + var request = &postRequest{ + Body: reader, + Url: "https://api.mch.weixin.qq.com/pay/profitsharingquery", + ContentType: e.PostContentType, + } + response, err := postToWx(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + result := ParseXMLReader(response.Body) + if returnCode, _ := result.GetString("return_code"); returnCode != "SUCCESS" { + returnMsg, _ := result.GetString("return_msg") + return nil, errors.New(returnMsg) + } + + if resultCode, _ := result.GetString("result_code"); resultCode != "SUCCESS" { + errDes, _ := result.GetString("err_code_des") + return nil, errors.New(errDes) + } + sign = result.Sign(signType) + if wxSign, _ := result.GetString("sign"); sign != wxSign { + return nil, e.ErrCheckSign + } + return result, nil +} + +func (m *myPayer) AddProfitSharingReceiver(param Param) (ResultParam, error) { + if param == nil { + return nil, e.ErrParams + } + if err := m.checkForPay(); err != nil { + return nil, err + } + + param.Add("appid", m.appId) + param.Add("mch_id", m.mchId) + + var ( + mustParam = []string{"mch_id", "appid", "nonce_str", "sign", "receiver"} + optionalParam = []string{"sign_type"} + signType = e.SignType256 + ) + if t, ok := param["sign_type"]; ok { + if t.(string) != e.SignType256 { + return nil, errors.New("only support HMAC-SHA256") + } + } + for _, v := range mustParam { + if v == "sign" { + continue + } + if _, ok := param[v]; !ok { + return nil, errors.New("need param: " + v) + } + } + + for key := range param { + if !util.HaveInArray(mustParam, key) && !util.HaveInArray(optionalParam, key) { + return nil, errors.New("no need param: " + key) + } + } + + sign := param.Sign(signType) + param.Add("sign", sign) + + reader, err := param.MarshalXML() + if err != nil { + return nil, err + } + var request = &postRequest{ + Body: reader, + Url: "https://api.mch.weixin.qq.com/pay/profitsharingaddreceiver", + ContentType: e.PostContentType, + } + response, err := postToWx(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + result := ParseXMLReader(response.Body) + if returnCode, _ := result.GetString("return_code"); returnCode != "SUCCESS" { + returnMsg, _ := result.GetString("return_msg") + return nil, errors.New(returnMsg) + } + + if resultCode, _ := result.GetString("result_code"); resultCode != "SUCCESS" { + errDes, _ := result.GetString("err_code_des") + return nil, errors.New(errDes) + } + sign = result.Sign(signType) + if wxSign, _ := result.GetString("sign"); sign != wxSign { + return nil, e.ErrCheckSign + } + return result, nil +} + +func (m *myPayer) RemoveProfitSharingReceiver(param Param) (ResultParam, error) { + if param == nil { + return nil, e.ErrParams + } + if err := m.checkForPay(); err != nil { + return nil, err + } + + param.Add("appid", m.appId) + param.Add("mch_id", m.mchId) + + var ( + mustParam = []string{"mch_id", "appid", "nonce_str", "sign", "receiver"} + optionalParam = []string{"sign_type"} + signType = e.SignType256 + ) + if t, ok := param["sign_type"]; ok { + if t.(string) != e.SignType256 { + return nil, errors.New("only support HMAC-SHA256") + } + } + for _, v := range mustParam { + if v == "sign" { + continue + } + if _, ok := param[v]; !ok { + return nil, errors.New("need param: " + v) + } + } + + for key := range param { + if !util.HaveInArray(mustParam, key) && !util.HaveInArray(optionalParam, key) { + return nil, errors.New("no need param: " + key) + } + } + + sign := param.Sign(signType) + param.Add("sign", sign) + + reader, err := param.MarshalXML() + if err != nil { + return nil, err + } + var request = &postRequest{ + Body: reader, + Url: "https://api.mch.weixin.qq.com/pay/profitsharingremovereceiver", + ContentType: e.PostContentType, + } + response, err := postToWx(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + result := ParseXMLReader(response.Body) + if returnCode, _ := result.GetString("return_code"); returnCode != "SUCCESS" { + returnMsg, _ := result.GetString("return_msg") + return nil, errors.New(returnMsg) + } + + if resultCode, _ := result.GetString("result_code"); resultCode != "SUCCESS" { + errDes, _ := result.GetString("err_code_des") + return nil, errors.New(errDes) + } + sign = result.Sign(signType) + if wxSign, _ := result.GetString("sign"); sign != wxSign { + return nil, e.ErrCheckSign + } + return result, nil +} + +func (m *myPayer) FinishProfitSharing(param Param, p12CertPath string) (ResultParam, error) { + if param == nil { + return nil, e.ErrParams + } + if err := m.checkForPay(); err != nil { + return nil, err + } + + //读取证书 + cert, err := util.P12ToPem(p12CertPath, m.mchId) + if err != nil { + return nil, err + } + + param.Add("appid", m.appId) + param.Add("mch_id", m.mchId) + + var ( + mustParam = []string{"mch_id", "appid", "nonce_str", "sign", "transaction_id", "out_order_no", "description"} + optionalParam = []string{"sign_type"} + signType = e.SignType256 + ) + + if t, ok := param["sign_type"]; ok { + if t.(string) != e.SignType256 { + return nil, errors.New("only support HMAC-SHA256") + } + } + + for _, v := range mustParam { + if v == "sign" { + continue + } + if _, ok := param[v]; !ok { + return nil, errors.New("need param: " + v) + } + } + for key := range param { + if !util.HaveInArray(mustParam, key) && !util.HaveInArray(optionalParam, key) { + return nil, errors.New("no need param: " + key) + } + } + + sign := param.Sign(signType) + param.Add("sign", sign) + + reader, err := param.MarshalXML() + if err != nil { + return nil, err + } + + var request = &postRequest{ + Body: reader, + Url: "https://api.mch.weixin.qq.com/secapi/pay/profitsharingfinish", + ContentType: e.PostContentType, + } + response, err := postToWxWithCert(request, cert) + if err != nil { + return nil, err + } + defer response.Body.Close() + result := ParseXMLReader(response.Body) + if returnCode, _ := result.GetString("return_code"); returnCode != "SUCCESS" { + returnMsg, _ := result.GetString("return_msg") + return nil, errors.New(returnMsg) + } + if resultCode, _ := result.GetString("result_code"); resultCode != "SUCCESS" { + errDes, _ := result.GetString("err_code_des") + return nil, errors.New(errDes) + } + sign = result.Sign(signType) + if resultSign, _ := result.GetString("sign"); resultSign != sign { + return nil, e.ErrCheckSign + } + return result, nil +} + +func (m *myPayer) ReturnProfitSharing(param Param, p12CertPath string) (ResultParam, error) { + if param == nil { + return nil, e.ErrParams + } + if err := m.checkForPay(); err != nil { + return nil, err + } + + //读取证书 + cert, err := util.P12ToPem(p12CertPath, m.mchId) + if err != nil { + return nil, err + } + + param.Add("appid", m.appId) + param.Add("mch_id", m.mchId) + + var ( + mustParam = []string{"mch_id", "appid", "nonce_str", "sign", "out_return_no", "return_account_type", "return_account", "return_amount", "description"} + oneParam = []string{"order_id", "out_order_no"} + optionalParam = []string{"sign_type"} + signType = e.SignType256 + ) + + if t, ok := param["sign_type"]; ok { + if t.(string) != e.SignType256 { + return nil, errors.New("only support HMAC-SHA256") + } + } + + var count = 0 + for _, k := range oneParam { + if v := param.Get(k); v != nil { + count++ + } + } + if count == 0 { + return nil, errors.New("need order number: order_id or out_order_no") + } else if count > 1 { + return nil, errors.New("just one order number: order_id or out_order_no") + } + + for _, v := range mustParam { + if v == "sign" { + continue + } + if _, ok := param[v]; !ok { + return nil, errors.New("need param: " + v) + } + } + for key := range param { + if !util.HaveInArray(mustParam, key) && !util.HaveInArray(optionalParam, key) && !util.HaveInArray(oneParam, key) { + return nil, errors.New("no need param: " + key) + } + } + + sign := param.Sign(signType) + param.Add("sign", sign) + + reader, err := param.MarshalXML() + if err != nil { + return nil, err + } + + var request = &postRequest{ + Body: reader, + Url: "https://api.mch.weixin.qq.com/secapi/pay/profitsharingreturn", + ContentType: e.PostContentType, + } + response, err := postToWxWithCert(request, cert) + if err != nil { + return nil, err + } + defer response.Body.Close() + result := ParseXMLReader(response.Body) + if returnCode, _ := result.GetString("return_code"); returnCode != "SUCCESS" { + returnMsg, _ := result.GetString("return_msg") + return nil, errors.New(returnMsg) + } + if resultCode, _ := result.GetString("result_code"); resultCode != "SUCCESS" { + errDes, _ := result.GetString("err_code_des") + return nil, errors.New(errDes) + } + sign = result.Sign(signType) + if resultSign, _ := result.GetString("sign"); resultSign != sign { + return nil, e.ErrCheckSign + } + return result, nil +} + +func (m *myPayer) QueryProfitSharingReturn(param Param) (ResultParam, error) { + if param == nil { + return nil, e.ErrParams + } + if err := m.checkForPay(); err != nil { + return nil, err + } + + param.Add("appid", m.appId) + param.Add("mch_id", m.mchId) + + var ( + mustParam = []string{"mch_id", "appid", "nonce_str", "sign", "out_return_no"} + oneParam = []string{"order_id", "out_order_no"} + optionalParam = []string{"sign_type"} + signType = e.SignType256 + ) + if t, ok := param["sign_type"]; ok { + if t.(string) != e.SignType256 { + return nil, errors.New("only support HMAC-SHA256") + } + } + + var count = 0 + for _, k := range oneParam { + if v := param.Get(k); v != nil { + count++ + } + } + if count == 0 { + return nil, errors.New("need order number: order_id or out_order_no") + } else if count > 1 { + return nil, errors.New("just one order number: order_id or out_order_no") + } + + for _, v := range mustParam { + if v == "sign" { + continue + } + if _, ok := param[v]; !ok { + return nil, errors.New("need param: " + v) + } + } + + for key := range param { + if !util.HaveInArray(mustParam, key) && !util.HaveInArray(optionalParam, key) && !util.HaveInArray(oneParam, key) { + return nil, errors.New("no need param: " + key) + } + } + + sign := param.Sign(signType) + param.Add("sign", sign) + + reader, err := param.MarshalXML() + if err != nil { + return nil, err + } + var request = &postRequest{ + Body: reader, + Url: "https://api.mch.weixin.qq.com/pay/profitsharingreturnquery", + ContentType: e.PostContentType, + } + response, err := postToWx(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + result := ParseXMLReader(response.Body) + if returnCode, _ := result.GetString("return_code"); returnCode != "SUCCESS" { + returnMsg, _ := result.GetString("return_msg") + return nil, errors.New(returnMsg) + } + + if resultCode, _ := result.GetString("result_code"); resultCode != "SUCCESS" { + errDes, _ := result.GetString("err_code_des") + return nil, errors.New(errDes) + } + sign = result.Sign(signType) + if wxSign, _ := result.GetString("sign"); sign != wxSign { + return nil, e.ErrCheckSign + } + return result, nil +} + +func (m *myPayer) ProfitSharingNotify(body io.Reader) (ResultParam, error) { + if len(m.apiV3Key) == 0 { + return nil, errors.New("no api v3 key") + } + bytes, err := ioutil.ReadAll(body) + if err != nil { + return nil, err + } + var data *profitSharingResult + if err = json.Unmarshal(bytes, &data); err != nil { + return nil, err + } + + if data == nil { + return nil, errors.New("read from body fail") + } + + var p = newResultMap() + p.Add("id", data.Id) + p.Add("create_time", data.CreateTime) + p.Add("event_type", data.EventType) + p.Add("summary", data.Summary) + p.Add("resource_type", data.ResourceType) + p.Add("algorithm", data.Resource.Algorithm) + p.Add("original_type", data.Resource.OriginalType) + p.Add("ciphertext", data.Resource.Ciphertext) + p.Add("associated_data", data.Resource.AssociatedData) + p.Add("nonce", data.Resource.Nonce) + + //对密文执行解密 + c := secret.NewCipher() + c.SetGCMNonce([]byte(data.Resource.Nonce)) + request := &secret.SymRequest{ + CipherData: data.Resource.Ciphertext, + Key: []byte(m.apiV3Key), + ModeType: secret.BlockModeGCM, + AddData: []byte(data.Resource.AssociatedData), + Type: secret.SymTypeAES, + } + plainBytes, err := c.SymDecrypt(request) + if err != nil { + return nil, err + } + var plainData *plain + if err = json.Unmarshal(plainBytes, &plainData); err != nil { + return nil, err + } + p.Add("mchid", plainData.Mchid) + p.Add("sp_mchid", plainData.SpMchid) + p.Add("sub_mchid", plainData.SubMchid) + p.Add("transaction_id", plainData.TransactionId) + p.Add("order_id", plainData.OrderId) + p.Add("out_order_no", plainData.OutOrderNo) + p.Add("type", plainData.Receiver.Type) + p.Add("account", plainData.Receiver.Account) + p.Add("amount", fmt.Sprintf("%v", plainData.Receiver.Amount)) + p.Add("description", plainData.Receiver.Description) + p.Add("success_time", plainData.Receiver.SuccessTime) + return p, nil +} + +type profitSharingResult struct { + Id string `json:"id"` + CreateTime string `json:"create_time"` + EventType string `json:"event_type"` + Summary string `json:"summary"` + ResourceType string `json:"resource_type"` + Resource *resource `json:"resource"` +} + +type resource struct { + Algorithm string `json:"algorithm"` + OriginalType string `json:"original_type"` + Ciphertext string `json:"ciphertext"` + AssociatedData string `json:"associated_data"` + Nonce string `json:"nonce"` +} + +type plain struct { + Mchid string `json:"mchid"` + SpMchid string `json:"sp_mchid"` + SubMchid string `json:"sub_mchid"` + TransactionId string `json:"transaction_id"` + OrderId string `json:"order_id"` + OutOrderNo string `json:"out_order_no"` + Receiver *receiver `json:"receiver"` +} + +type receiver struct { + Type string `json:"type"` + Account string `json:"account"` + Amount int64 `json:"amount"` + Description string `json:"description"` + SuccessTime string `json:"success_time"` +}