mch_transfer.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package wx_mp
  2. import (
  3. "bytes"
  4. "crypto"
  5. "crypto/rand"
  6. "crypto/rsa"
  7. "crypto/sha256"
  8. "crypto/x509"
  9. "encoding/base64"
  10. "encoding/json"
  11. "encoding/pem"
  12. "fmt"
  13. "io/ioutil"
  14. "net/http"
  15. "net/url"
  16. "strings"
  17. "time"
  18. "github.com/astaxie/beego"
  19. "github.com/uuid"
  20. )
  21. const (
  22. MerchantTransferAccepted = "ACCEPTED"
  23. MerchantTransferProcessing = "PROCESSING"
  24. MerchantTransferWaitUserConfirm = "WAIT_USER_CONFIRM"
  25. MerchantTransferTransfering = "TRANSFERING"
  26. MerchantTransferSuccess = "SUCCESS"
  27. MerchantTransferFail = "FAIL"
  28. MerchantTransferCancelled = "CANCELLED"
  29. MerchantTransferCanceling = "CANCELING"
  30. )
  31. type MerchantTransferReportInfo struct {
  32. InfoType string `json:"info_type"`
  33. InfoContent string `json:"info_content"`
  34. }
  35. type MerchantTransferRequest struct {
  36. AppId string `json:"appid"`
  37. OutBillNo string `json:"out_bill_no"`
  38. TransferSceneId string `json:"transfer_scene_id"`
  39. OpenId string `json:"openid"`
  40. TransferAmount int64 `json:"transfer_amount"`
  41. TransferRemark string `json:"transfer_remark"`
  42. NotifyUrl string `json:"notify_url,omitempty"`
  43. UserRecvPerception string `json:"user_recv_perception,omitempty"`
  44. TransferSceneReportInfos []MerchantTransferReportInfo `json:"transfer_scene_report_infos,omitempty"`
  45. }
  46. type MerchantTransferResponse struct {
  47. OutBillNo string `json:"out_bill_no"`
  48. TransferBillNo string `json:"transfer_bill_no"`
  49. CreateTime string `json:"create_time"`
  50. State string `json:"state"`
  51. FailReason string `json:"fail_reason"`
  52. PackageInfo string `json:"package_info"`
  53. Code string `json:"code"`
  54. Message string `json:"message"`
  55. RawBody string `json:"-"`
  56. HttpStatus int `json:"-"`
  57. }
  58. func MerchantTransfer(openid string, amount int64, outBillNo, payCode, remark string) (*MerchantTransferResponse, error) {
  59. appId := beego.AppConfig.String("WxFohowXcxAppId")
  60. mechantInfo := GetMechantInfo(payCode)
  61. if remark == "" {
  62. remark = merchantTransferConfig("MchTransferRemark", "佣金提现")
  63. }
  64. req := MerchantTransferRequest{
  65. AppId: appId,
  66. OutBillNo: outBillNo,
  67. TransferSceneId: merchantTransferConfig("MchTransferSceneId", "1005"),
  68. OpenId: openid,
  69. TransferAmount: amount,
  70. TransferRemark: remark,
  71. NotifyUrl: merchantTransferConfig("MchTransferNotifyUrl", ""),
  72. UserRecvPerception: merchantTransferConfig("MchTransferUserRecvPerception", "劳务报酬"),
  73. TransferSceneReportInfos: merchantTransferReportInfos(),
  74. }
  75. body, err := json.Marshal(req)
  76. if err != nil {
  77. return nil, err
  78. }
  79. return doMerchantTransferRequest("POST", "/v3/fund-app/mch-transfer/transfer-bills", body, mechantInfo)
  80. }
  81. func QueryMerchantTransferByOutBillNo(outBillNo, payCode string) (*MerchantTransferResponse, error) {
  82. mechantInfo := GetMechantInfo(payCode)
  83. path := "/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/" + url.PathEscape(outBillNo)
  84. return doMerchantTransferRequest("GET", path, nil, mechantInfo)
  85. }
  86. func CancelMerchantTransferByOutBillNo(outBillNo, payCode string) (*MerchantTransferResponse, error) {
  87. mechantInfo := GetMechantInfo(payCode)
  88. path := "/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/" + url.PathEscape(outBillNo) + "/cancel"
  89. return doMerchantTransferRequest("POST", path, nil, mechantInfo)
  90. }
  91. func MerchantTransferStatePending(state string) bool {
  92. switch state {
  93. case MerchantTransferAccepted, MerchantTransferProcessing, MerchantTransferWaitUserConfirm, MerchantTransferTransfering, MerchantTransferCanceling:
  94. return true
  95. default:
  96. return false
  97. }
  98. }
  99. func merchantTransferConfig(name, def string) string {
  100. value := strings.TrimSpace(beego.AppConfig.String(name))
  101. if value == "" {
  102. return def
  103. }
  104. return value
  105. }
  106. func merchantTransferReportInfos() []MerchantTransferReportInfo {
  107. raw := merchantTransferConfig("MchTransferReportInfos", "岗位类型:推广员,报酬说明:佣金提现")
  108. if raw == "" {
  109. return nil
  110. }
  111. var list []MerchantTransferReportInfo
  112. for _, item := range strings.Split(raw, ",") {
  113. item = strings.TrimSpace(item)
  114. if item == "" {
  115. continue
  116. }
  117. parts := strings.SplitN(item, ":", 2)
  118. if len(parts) != 2 {
  119. continue
  120. }
  121. infoType := strings.TrimSpace(parts[0])
  122. infoContent := strings.TrimSpace(parts[1])
  123. if infoType != "" && infoContent != "" {
  124. list = append(list, MerchantTransferReportInfo{InfoType: infoType, InfoContent: infoContent})
  125. }
  126. }
  127. return list
  128. }
  129. func doMerchantTransferRequest(method, path string, body []byte, mechantInfo MechantPayInfo) (*MerchantTransferResponse, error) {
  130. timestamp := fmt.Sprintf("%d", time.Now().Unix())
  131. nonce := strings.Replace(uuid.NewV4().String(), "-", "", -1)
  132. bodyText := ""
  133. if len(body) > 0 {
  134. bodyText = string(body)
  135. }
  136. signature, serialNo, err := signWechatPayV3Request(method, path, timestamp, nonce, bodyText, mechantInfo)
  137. if err != nil {
  138. return nil, err
  139. }
  140. req, err := http.NewRequest(method, "https://api.mch.weixin.qq.com"+path, bytes.NewReader(body))
  141. if err != nil {
  142. return nil, err
  143. }
  144. req.Header.Set("Accept", "application/json")
  145. req.Header.Set("Content-Type", "application/json")
  146. 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))
  147. resp, err := http.DefaultClient.Do(req)
  148. if err != nil {
  149. return nil, err
  150. }
  151. defer resp.Body.Close()
  152. respBody, _ := ioutil.ReadAll(resp.Body)
  153. ret := &MerchantTransferResponse{RawBody: string(respBody), HttpStatus: resp.StatusCode}
  154. if len(respBody) > 0 {
  155. _ = json.Unmarshal(respBody, ret)
  156. }
  157. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  158. if ret.Message == "" {
  159. ret.Message = string(respBody)
  160. }
  161. return ret, fmt.Errorf("wechat merchant transfer status=%d code=%s message=%s", resp.StatusCode, ret.Code, ret.Message)
  162. }
  163. return ret, nil
  164. }
  165. func signWechatPayV3Request(method, path, timestamp, nonce, body string, mechantInfo MechantPayInfo) (string, string, error) {
  166. privateKey, err := loadMerchantPrivateKey(mechantInfo.MchKeyFile)
  167. if err != nil {
  168. return "", "", err
  169. }
  170. serialNo, err := loadMerchantCertSerialNo(mechantInfo.MchCertFile)
  171. if err != nil {
  172. return "", "", err
  173. }
  174. message := strings.ToUpper(method) + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body + "\n"
  175. h := sha256.Sum256([]byte(message))
  176. signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, h[:])
  177. if err != nil {
  178. return "", "", err
  179. }
  180. return base64.StdEncoding.EncodeToString(signature), serialNo, nil
  181. }
  182. func loadMerchantPrivateKey(filename string) (*rsa.PrivateKey, error) {
  183. keyData, err := ioutil.ReadFile(filename)
  184. if err != nil {
  185. return nil, err
  186. }
  187. block, _ := pem.Decode(keyData)
  188. if block == nil {
  189. return nil, fmt.Errorf("invalid merchant private key pem")
  190. }
  191. switch block.Type {
  192. case "PRIVATE KEY":
  193. key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
  194. if err != nil {
  195. return nil, err
  196. }
  197. privateKey, ok := key.(*rsa.PrivateKey)
  198. if !ok {
  199. return nil, fmt.Errorf("merchant private key is not rsa")
  200. }
  201. return privateKey, nil
  202. case "RSA PRIVATE KEY":
  203. return x509.ParsePKCS1PrivateKey(block.Bytes)
  204. default:
  205. return nil, fmt.Errorf("unsupported private key type: %s", block.Type)
  206. }
  207. }
  208. func loadMerchantCertSerialNo(filename string) (string, error) {
  209. certData, err := ioutil.ReadFile(filename)
  210. if err != nil {
  211. return "", err
  212. }
  213. block, _ := pem.Decode(certData)
  214. if block == nil {
  215. return "", fmt.Errorf("invalid merchant cert pem")
  216. }
  217. cert, err := x509.ParseCertificate(block.Bytes)
  218. if err != nil {
  219. return "", err
  220. }
  221. return strings.ToUpper(cert.SerialNumber.Text(16)), nil
  222. }