package wx_mp import ( "bytes" "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "io/ioutil" "net/http" "net/url" "strings" "time" "github.com/astaxie/beego" "github.com/uuid" ) const ( MerchantTransferAccepted = "ACCEPTED" MerchantTransferProcessing = "PROCESSING" MerchantTransferWaitUserConfirm = "WAIT_USER_CONFIRM" MerchantTransferTransfering = "TRANSFERING" MerchantTransferSuccess = "SUCCESS" MerchantTransferFail = "FAIL" MerchantTransferCancelled = "CANCELLED" MerchantTransferCanceling = "CANCELING" ) type MerchantTransferReportInfo struct { InfoType string `json:"info_type"` InfoContent string `json:"info_content"` } type MerchantTransferRequest struct { AppId string `json:"appid"` OutBillNo string `json:"out_bill_no"` TransferSceneId string `json:"transfer_scene_id"` OpenId string `json:"openid"` TransferAmount int64 `json:"transfer_amount"` TransferRemark string `json:"transfer_remark"` NotifyUrl string `json:"notify_url,omitempty"` UserRecvPerception string `json:"user_recv_perception,omitempty"` TransferSceneReportInfos []MerchantTransferReportInfo `json:"transfer_scene_report_infos,omitempty"` } type MerchantTransferResponse struct { OutBillNo string `json:"out_bill_no"` TransferBillNo string `json:"transfer_bill_no"` CreateTime string `json:"create_time"` State string `json:"state"` FailReason string `json:"fail_reason"` PackageInfo string `json:"package_info"` Code string `json:"code"` Message string `json:"message"` RawBody string `json:"-"` HttpStatus int `json:"-"` } func MerchantTransfer(openid string, amount int64, outBillNo, payCode, remark string) (*MerchantTransferResponse, error) { appId := beego.AppConfig.String("WxFohowXcxAppId") mechantInfo := GetMechantInfo(payCode) if remark == "" { remark = merchantTransferConfig("MchTransferRemark", "佣金提现") } req := MerchantTransferRequest{ AppId: appId, OutBillNo: outBillNo, TransferSceneId: merchantTransferConfig("MchTransferSceneId", "1005"), OpenId: openid, TransferAmount: amount, TransferRemark: remark, NotifyUrl: merchantTransferConfig("MchTransferNotifyUrl", ""), UserRecvPerception: merchantTransferConfig("MchTransferUserRecvPerception", "劳务报酬"), TransferSceneReportInfos: merchantTransferReportInfos(), } body, err := json.Marshal(req) if err != nil { return nil, err } return doMerchantTransferRequest("POST", "/v3/fund-app/mch-transfer/transfer-bills", body, mechantInfo) } func QueryMerchantTransferByOutBillNo(outBillNo, payCode string) (*MerchantTransferResponse, error) { mechantInfo := GetMechantInfo(payCode) path := "/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/" + url.PathEscape(outBillNo) return doMerchantTransferRequest("GET", path, nil, mechantInfo) } func CancelMerchantTransferByOutBillNo(outBillNo, payCode string) (*MerchantTransferResponse, error) { mechantInfo := GetMechantInfo(payCode) path := "/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/" + url.PathEscape(outBillNo) + "/cancel" return doMerchantTransferRequest("POST", path, nil, mechantInfo) } func MerchantTransferStatePending(state string) bool { switch state { case MerchantTransferAccepted, MerchantTransferProcessing, MerchantTransferWaitUserConfirm, MerchantTransferTransfering, MerchantTransferCanceling: return true default: return false } } func merchantTransferConfig(name, def string) string { value := strings.TrimSpace(beego.AppConfig.String(name)) if value == "" { return def } return value } func merchantTransferReportInfos() []MerchantTransferReportInfo { raw := merchantTransferConfig("MchTransferReportInfos", "岗位类型:推广员,报酬说明:佣金提现") if raw == "" { return nil } var list []MerchantTransferReportInfo for _, item := range strings.Split(raw, ",") { item = strings.TrimSpace(item) if item == "" { continue } parts := strings.SplitN(item, ":", 2) if len(parts) != 2 { continue } infoType := strings.TrimSpace(parts[0]) infoContent := strings.TrimSpace(parts[1]) if infoType != "" && infoContent != "" { list = append(list, MerchantTransferReportInfo{InfoType: infoType, InfoContent: infoContent}) } } return list } func doMerchantTransferRequest(method, path string, body []byte, mechantInfo MechantPayInfo) (*MerchantTransferResponse, error) { timestamp := fmt.Sprintf("%d", time.Now().Unix()) nonce := strings.Replace(uuid.NewV4().String(), "-", "", -1) bodyText := "" if len(body) > 0 { bodyText = string(body) } signature, serialNo, err := signWechatPayV3Request(method, path, timestamp, nonce, bodyText, mechantInfo) if err != nil { return nil, err } req, err := http.NewRequest(method, "https://api.mch.weixin.qq.com"+path, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf(`WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"`, mechantInfo.MchId, nonce, signature, timestamp, serialNo)) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() respBody, _ := ioutil.ReadAll(resp.Body) ret := &MerchantTransferResponse{RawBody: string(respBody), HttpStatus: resp.StatusCode} if len(respBody) > 0 { _ = json.Unmarshal(respBody, ret) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { if ret.Message == "" { ret.Message = string(respBody) } return ret, fmt.Errorf("wechat merchant transfer status=%d code=%s message=%s", resp.StatusCode, ret.Code, ret.Message) } return ret, nil } func signWechatPayV3Request(method, path, timestamp, nonce, body string, mechantInfo MechantPayInfo) (string, string, error) { privateKey, err := loadMerchantPrivateKey(mechantInfo.MchKeyFile) if err != nil { return "", "", err } serialNo, err := loadMerchantCertSerialNo(mechantInfo.MchCertFile) if err != nil { return "", "", err } message := strings.ToUpper(method) + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body + "\n" h := sha256.Sum256([]byte(message)) signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, h[:]) if err != nil { return "", "", err } return base64.StdEncoding.EncodeToString(signature), serialNo, nil } func loadMerchantPrivateKey(filename string) (*rsa.PrivateKey, error) { keyData, err := ioutil.ReadFile(filename) if err != nil { return nil, err } block, _ := pem.Decode(keyData) if block == nil { return nil, fmt.Errorf("invalid merchant private key pem") } switch block.Type { case "PRIVATE KEY": key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, err } privateKey, ok := key.(*rsa.PrivateKey) if !ok { return nil, fmt.Errorf("merchant private key is not rsa") } return privateKey, nil case "RSA PRIVATE KEY": return x509.ParsePKCS1PrivateKey(block.Bytes) default: return nil, fmt.Errorf("unsupported private key type: %s", block.Type) } } func loadMerchantCertSerialNo(filename string) (string, error) { certData, err := ioutil.ReadFile(filename) if err != nil { return "", err } block, _ := pem.Decode(certData) if block == nil { return "", fmt.Errorf("invalid merchant cert pem") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return "", err } return strings.ToUpper(cert.SerialNumber.Text(16)), nil }