From 45c280d8d6ae14efbc9b9e4e9539be547deeceac Mon Sep 17 00:00:00 2001 From: gazebo Date: Tue, 29 Oct 2019 11:38:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=85=BE=E8=AE=AF=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- platformapi/platformapi.go | 5 +- platformapi/wxpay/callback.go | 63 +++++++++ platformapi/wxpay/wxpay.go | 238 ++++++++++++++++++++++++++++++++ platformapi/wxpay/wxpay_test.go | 31 +++++ 4 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 platformapi/wxpay/callback.go create mode 100644 platformapi/wxpay/wxpay.go create mode 100644 platformapi/wxpay/wxpay_test.go diff --git a/platformapi/platformapi.go b/platformapi/platformapi.go index 1bafbbaa..d6943b2d 100644 --- a/platformapi/platformapi.go +++ b/platformapi/platformapi.go @@ -166,7 +166,10 @@ func AccessPlatformAPIWithRetry(client *http.Client, handleRequest func() *http. // 临时处理返回值居然不是map的情况 if bodyMap2, ok := bodyGeneral.(map[string]interface{}); ok { bodyMap = bodyMap2 - } else if bodyGeneral != nil { + } else { + if bodyGeneral == nil { + bodyGeneral = string(bodyData) + } bodyMap = map[string]interface{}{ KeyData: bodyGeneral, } diff --git a/platformapi/wxpay/callback.go b/platformapi/wxpay/callback.go new file mode 100644 index 00000000..caa3fdcb --- /dev/null +++ b/platformapi/wxpay/callback.go @@ -0,0 +1,63 @@ +package wxpay + +import ( + "encoding/xml" + "fmt" + "io/ioutil" + "net/http" + + "git.rosy.net.cn/baseapi/utils" +) + +type CDData string + +func (c CDData) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + return e.EncodeElement(struct { + string `xml:",cdata"` + }{string(c)}, start) +} + +type CallbackResponse struct { + XMLName xml.Name `json:"-" xml:"xml"` + + ReturnCode string `json:"return_code" xml:"return_code"` + ReturnMsg CDData `json:"return_msg" xml:"return_msg"` +} + +var ( + SuccessResponse = &CallbackResponse{ + ReturnCode: ResponseCodeSuccess, + ReturnMsg: "OK", + } +) + +func Err2CallbackResponse(err error, data string) *CallbackResponse { + if err == nil { + return SuccessResponse + } + returnMsg := err.Error() + if data != "" { + returnMsg = fmt.Sprintf("%s,%s", returnMsg, data) + } + return &CallbackResponse{ + ReturnCode: ResponseCodeFail, + ReturnMsg: CDData(returnMsg), + } +} + +func (a *API) GetCallbackMsg(request *http.Request) (msg map[string]interface{}, callbackResponse *CallbackResponse) { + data, err := ioutil.ReadAll(request.Body) + if err != nil { + return nil, Err2CallbackResponse(err, "") + } + msg, _, err = a.checkResultAsMap(string(data)) + if err != nil { + return nil, Err2CallbackResponse(err, "") + } + sign := utils.Interface2String(msg[sigKey]) + desiredSign := a.signParam(msg) + if desiredSign != sign { + return nil, Err2CallbackResponse(fmt.Errorf("desiredSign:%s <> sign:%s", desiredSign, sign), "") + } + return msg, nil +} diff --git a/platformapi/wxpay/wxpay.go b/platformapi/wxpay/wxpay.go new file mode 100644 index 00000000..bfd98e53 --- /dev/null +++ b/platformapi/wxpay/wxpay.go @@ -0,0 +1,238 @@ +package wxpay + +import ( + "bytes" + "crypto/md5" + "encoding/xml" + "fmt" + "net/http" + "sort" + "strings" + + "git.rosy.net.cn/baseapi/platformapi" + "git.rosy.net.cn/baseapi/utils" + "github.com/clbanning/mxj" +) + +const ( + prodURL = "https://api.mch.weixin.qq.com/pay" + sandboxURL = "https://api.mch.weixin.qq.com/sandboxnew/pay" +) + +const ( + ResponseCodeSuccess = "SUCCESS" + ResponseCodeFail = "FAIL" + + sigKey = "sign" + sigTypeKey = "sign_type" + sigType = "MD5" +) + +type RequestBase struct { + XMLName xml.Name `json:"-" xml:"xml"` + + AppID string `json:"appid" xml:"appid"` + MchID string `json:"mch_id" xml:"mch_id"` + NonceStr string `json:"nonce_str" xml:"nonce_str"` + Sign string `json:"sign" xml:"sign"` + SignType string `json:"sign_type,omitempty" xml:"sign_type,omitempty"` +} + +type ResponseResult struct { + XMLName xml.Name `json:"-" xml:"xml"` + + ReturnCode string `json:"return_code" xml:"return_code"` + ReturnMsg string `json:"return_msg" xml:"return_msg"` + + AppID string `json:"appid" xml:"appid"` + MchID string `json:"mch_id" xml:"mch_id"` + NonceStr string `json:"nonce_str" xml:"nonce_str"` + Sign string `json:"sign" xml:"sign"` + SignType string `json:"sign_type,omitempty" xml:"sign_type,omitempty"` + ResultCode string `json:"result_code" xml:"result_code"` + ErrCode string `json:"err_code,omitempty" xml:"err_code,omitempty"` + ErrCodeDes string `json:"err_code_des,omitempty" xml:"err_code_des,omitempty"` + + DeviceInfo string `json:"device_info,omitempty" xml:"device_info,omitempty"` + OpenID string `json:"openid,omitempty" xml:"openid,omitempty"` +} + +type OrderQueryParam struct { + RequestBase + TransactionID string `json:"transaction_id" xml:"transaction_id"` + OutTradeNo string `json:"out_trade_no" xml:"out_trade_no"` +} + +type API struct { + appID string + appKey string + mchID string + client *http.Client + config *platformapi.APIConfig +} + +func New(appID, appKey, mchID string, config ...*platformapi.APIConfig) *API { + curConfig := platformapi.DefAPIConfig + if len(config) > 0 { + curConfig = *config[0] + } + return &API{ + appID: appID, + appKey: appKey, + mchID: mchID, + client: &http.Client{Timeout: curConfig.ClientTimeout}, + config: &curConfig, + } +} + +func (a *API) GetAppID() string { + return a.appID +} + +func (a *API) GetMchID() string { + return a.mchID +} + +func (a *API) signParam(params map[string]interface{}) (sig string) { + var valueList []string + for k, v := range params { + if k != sigKey { + if str := fmt.Sprint(v); str != "" { + valueList = append(valueList, fmt.Sprintf("%s=%s", k, str)) + } + } + } + sort.Sort(sort.StringSlice(valueList)) + valueList = append(valueList, fmt.Sprintf("key=%s", a.appKey)) + sig = strings.Join(valueList, "&") + sig = fmt.Sprintf("%X", md5.Sum([]byte(sig))) + return sig +} + +func unmarshalXML(data []byte, result interface{}) error { + d := xml.NewDecoder(bytes.NewReader(data)) + return d.Decode(result) +} + +func mustMarshalXML(obj interface{}) []byte { + byteArr, err := xml.Marshal(obj) + if err != nil { + panic(fmt.Sprintf("err when Marshal obj:%v with error:%v", obj, err)) + } + return byteArr +} + +func (a *API) AccessAPI(action string, params interface{}, baseParam *RequestBase) (retVal *ResponseResult, err error) { + baseParam.AppID = a.appID + baseParam.MchID = a.mchID + baseParam.NonceStr = utils.GetUUID() + baseParam.SignType = sigType + baseParam.Sign = a.signParam(utils.Struct2FlatMap(params)) + + fullURL := utils.GenerateGetURL(prodURL, action, nil) + + err = platformapi.AccessPlatformAPIWithRetry(a.client, + func() *http.Request { + request, _ := http.NewRequest(http.MethodPost, fullURL, bytes.NewReader(mustMarshalXML(params))) + return request + }, + a.config, + func(response *http.Response, bodyStr string, jsonResult1 map[string]interface{}) (errLevel string, err error) { + if jsonResult1 == nil { + return platformapi.ErrLevelRecoverableErr, fmt.Errorf("mapData is nil") + } + retVal, errLevel, err = a.checkResult(jsonResult1[platformapi.KeyData].(string)) + return errLevel, err + }) + return retVal, err +} + +func (a *API) checkResult(xmlStr string) (result *ResponseResult, errLevel string, err error) { + err = unmarshalXML([]byte(xmlStr), &result) + if err != nil { + errLevel = platformapi.ErrLevelGeneralFail + } else { + if result.ReturnCode != ResponseCodeSuccess { + errLevel = platformapi.ErrLevelGeneralFail + err = utils.NewErrorCode(result.ReturnMsg, result.ReturnCode) + result = nil + } else { + // if result.ResultCode != ResponseCodeSuccess { + // errLevel = platformapi.ErrLevelGeneralFail + // err = utils.NewErrorCode(result.ErrCodeDes, result.ErrCode) + // result = nil + // } else { + // } + } + } + return result, errLevel, err +} + +func (a *API) AccessAPIByMap(action string, params map[string]interface{}) (retVal map[string]interface{}, err error) { + params2 := utils.MergeMaps(params, map[string]interface{}{ + "appid": a.appID, + "mch_id": a.mchID, + "nonce_str": utils.GetUUID(), + "sign_type": sigType, + }) + params2[sigKey] = a.signParam(params2) + + fullURL := utils.GenerateGetURL(prodURL, action, nil) + xmlBytes, err := mxj.Map(params2).Xml("xml") + if err != nil { + return nil, err + } + err = platformapi.AccessPlatformAPIWithRetry(a.client, + func() *http.Request { + request, _ := http.NewRequest(http.MethodPost, fullURL, bytes.NewReader(xmlBytes)) + return request + }, + a.config, + func(response *http.Response, bodyStr string, jsonResult1 map[string]interface{}) (errLevel string, err error) { + if jsonResult1 == nil { + return platformapi.ErrLevelRecoverableErr, fmt.Errorf("mapData is nil") + } + retVal, errLevel, err = a.checkResultAsMap(jsonResult1[platformapi.KeyData].(string)) + return errLevel, err + }) + return retVal, err +} + +func (a *API) checkResultAsMap(xmlStr string) (result map[string]interface{}, errLevel string, err error) { + mv, err := mxj.NewMapXml([]byte(xmlStr)) + if err != nil { + errLevel = platformapi.ErrLevelGeneralFail + } else { + result = mv["xml"].(map[string]interface{}) + returnCode := utils.Interface2String(result["return_code"]) + if returnCode != ResponseCodeSuccess { + errLevel = platformapi.ErrLevelGeneralFail + err = utils.NewErrorCode(utils.Interface2String(result["return_msg"]), returnCode) + result = nil + } else { + // if utils.Interface2String(result["result_code"]) != ResponseCodeSuccess { + // errLevel = platformapi.ErrLevelGeneralFail + // err = utils.NewErrorCode(utils.Interface2String(result["err_code_desc"]), utils.Interface2String(result["err_code"])) + // result = nil + // } else { + // } + } + } + return result, errLevel, err +} + +func (a *API) OrderQuery(transactionID, outTradeNo string) (retVal map[string]interface{}, err error) { + // param := &OrderQueryParam{ + // TransactionID: transactionID, + // OutTradeNo: outTradeNo, + // } + // retVal, err = a.AccessAPI("orderquery", param, ¶m.RequestBase) + param := map[string]interface{}{} + if transactionID != "" { + param["transaction_id"] = transactionID + } else { + param["out_trade_no"] = outTradeNo + } + retVal, err = a.AccessAPIByMap("orderquery", param) + return retVal, err +} diff --git a/platformapi/wxpay/wxpay_test.go b/platformapi/wxpay/wxpay_test.go new file mode 100644 index 00000000..3a81c1c0 --- /dev/null +++ b/platformapi/wxpay/wxpay_test.go @@ -0,0 +1,31 @@ +package wxpay + +import ( + "testing" + + "git.rosy.net.cn/baseapi" + "git.rosy.net.cn/baseapi/utils" + + "go.uber.org/zap" +) + +var ( + api *API + sugarLogger *zap.SugaredLogger +) + +func init() { + logger, _ := zap.NewDevelopment() + sugarLogger = logger.Sugar() + baseapi.Init(sugarLogger) + + api = New("wx4b5930c13f8b1170", "XKJPOIHJ233adf01KJIXlIeQDSDKFJAD", "1390686702") +} + +func TestOrderQuery(t *testing.T) { + result, err := api.OrderQuery("4200000411201910186159598703", "") + if err != nil { + t.Fatal(err) + } + t.Log(utils.Format4Output(result, false)) +}