wechat_pay.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. package utils
  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. "errors"
  13. "fmt"
  14. "io"
  15. "net/http"
  16. "net/url"
  17. "os"
  18. "strconv"
  19. "strings"
  20. "time"
  21. )
  22. type WxPayClient struct {
  23. MchID string
  24. SerialNo string
  25. NotifyURL string
  26. JSAPIAppID string
  27. H5AppID string
  28. AppAppID string
  29. PrivateKey *rsa.PrivateKey
  30. HTTPClient *http.Client
  31. APIBase string
  32. Enabled bool
  33. UserAgent string
  34. }
  35. type WxPayAmount struct {
  36. Total int `json:"total"`
  37. Currency string `json:"currency,omitempty"`
  38. }
  39. type WxPayPayer struct {
  40. OpenID string `json:"openid,omitempty"`
  41. }
  42. type WxPayH5Info struct {
  43. Type string `json:"type"`
  44. }
  45. type WxPaySceneInfo struct {
  46. PayerClientIP string `json:"payer_client_ip,omitempty"`
  47. H5Info *WxPayH5Info `json:"h5_info,omitempty"`
  48. }
  49. type wxPayTxnRequest struct {
  50. AppID string `json:"appid"`
  51. MchID string `json:"mchid"`
  52. Description string `json:"description"`
  53. OutTradeNo string `json:"out_trade_no"`
  54. NotifyURL string `json:"notify_url"`
  55. Attach string `json:"attach,omitempty"`
  56. Amount WxPayAmount `json:"amount"`
  57. Payer *WxPayPayer `json:"payer,omitempty"`
  58. SceneInfo *WxPaySceneInfo `json:"scene_info,omitempty"`
  59. }
  60. type wxPayTxnResp struct {
  61. PrepayID string `json:"prepay_id"`
  62. H5URL string `json:"h5_url"`
  63. }
  64. type WxMiniProgramOrderReq struct {
  65. Description string
  66. OutTradeNo string
  67. Total int
  68. Currency string
  69. OpenID string
  70. NotifyURL string
  71. Attach string
  72. AppID string
  73. }
  74. type WxMiniProgramPayResult struct {
  75. AppID string `json:"appId"`
  76. TimeStamp string `json:"timeStamp"`
  77. NonceStr string `json:"nonceStr"`
  78. Package string `json:"package"`
  79. SignType string `json:"signType"`
  80. PaySign string `json:"paySign"`
  81. PrepayID string `json:"prepayId"`
  82. }
  83. type WxH5OrderReq struct {
  84. Description string
  85. OutTradeNo string
  86. Total int
  87. Currency string
  88. NotifyURL string
  89. Attach string
  90. PayerClientIP string
  91. AppID string
  92. }
  93. type WxH5PayResult struct {
  94. H5URL string `json:"h5Url"`
  95. }
  96. type WxAppOrderReq struct {
  97. Description string
  98. OutTradeNo string
  99. Total int
  100. Currency string
  101. NotifyURL string
  102. Attach string
  103. AppID string
  104. }
  105. type WxAppPayResult struct {
  106. AppID string `json:"appid"`
  107. PartnerID string `json:"partnerid"`
  108. PrepayID string `json:"prepayid"`
  109. Package string `json:"package"`
  110. NonceStr string `json:"noncestr"`
  111. TimeStamp string `json:"timestamp"`
  112. Sign string `json:"sign"`
  113. }
  114. var wxPayClient *WxPayClient
  115. func InitWxPay() error {
  116. enabled := redisBool("wxpay_enable", false)
  117. if !enabled {
  118. return nil
  119. }
  120. mchID := redisString("wxpay_mch_id", "")
  121. serialNo := redisString("wxpay_serial_no", "")
  122. privateKeyPath := redisString("wxpay_private_key_path", "")
  123. notifyURL := redisString("wxpay_notify_url", "")
  124. jsapiAppID := redisString("wxpay_jsapi_appid", "")
  125. h5AppID := redisString("wxpay_h5_appid", "")
  126. appAppID := redisString("wxpay_app_appid", "")
  127. apiBase := redisString("wxpay_api_base", "https://api.mch.weixin.qq.com")
  128. if mchID == "" || serialNo == "" || privateKeyPath == "" || notifyURL == "" {
  129. return errors.New("wxpay config missing: wxpay_mch_id/wxpay_serial_no/wxpay_private_key_path/wxpay_notify_url are required")
  130. }
  131. privateKey, err := loadRSAPrivateKey(privateKeyPath)
  132. if err != nil {
  133. return err
  134. }
  135. wxPayClient = &WxPayClient{
  136. MchID: mchID,
  137. SerialNo: serialNo,
  138. NotifyURL: notifyURL,
  139. JSAPIAppID: jsapiAppID,
  140. H5AppID: h5AppID,
  141. AppAppID: appAppID,
  142. PrivateKey: privateKey,
  143. HTTPClient: &http.Client{Timeout: 15 * time.Second},
  144. APIBase: strings.TrimRight(apiBase, "/"),
  145. Enabled: true,
  146. UserAgent: "think-go-wxpay/1.0",
  147. }
  148. return nil
  149. }
  150. func GetWxPayClient() (*WxPayClient, error) {
  151. if wxPayClient == nil || !wxPayClient.Enabled {
  152. return nil, errors.New("wxpay not initialized, set wxpay_enable=true and call InitWxPay")
  153. }
  154. return wxPayClient, nil
  155. }
  156. func CreateWxMiniProgramPay(req WxMiniProgramOrderReq) (*WxMiniProgramPayResult, error) {
  157. client, err := GetWxPayClient()
  158. if err != nil {
  159. return nil, err
  160. }
  161. return client.CreateMiniProgramPay(req)
  162. }
  163. func CreateWxH5Pay(req WxH5OrderReq) (*WxH5PayResult, error) {
  164. client, err := GetWxPayClient()
  165. if err != nil {
  166. return nil, err
  167. }
  168. return client.CreateH5Pay(req)
  169. }
  170. func CreateWxAppPay(req WxAppOrderReq) (*WxAppPayResult, error) {
  171. client, err := GetWxPayClient()
  172. if err != nil {
  173. return nil, err
  174. }
  175. return client.CreateAppPay(req)
  176. }
  177. func (c *WxPayClient) CreateMiniProgramPay(req WxMiniProgramOrderReq) (*WxMiniProgramPayResult, error) {
  178. appID := req.AppID
  179. if appID == "" {
  180. appID = c.JSAPIAppID
  181. }
  182. if appID == "" {
  183. return nil, errors.New("jsapi appid is required")
  184. }
  185. if req.OpenID == "" {
  186. return nil, errors.New("openid is required")
  187. }
  188. if req.OutTradeNo == "" || req.Description == "" || req.Total <= 0 {
  189. return nil, errors.New("description/outTradeNo/total are required")
  190. }
  191. requestBody := wxPayTxnRequest{
  192. AppID: appID,
  193. MchID: c.MchID,
  194. Description: req.Description,
  195. OutTradeNo: req.OutTradeNo,
  196. NotifyURL: firstNotEmpty(req.NotifyURL, c.NotifyURL),
  197. Attach: req.Attach,
  198. Amount: WxPayAmount{
  199. Total: req.Total,
  200. Currency: firstNotEmpty(req.Currency, "CNY"),
  201. },
  202. Payer: &WxPayPayer{OpenID: req.OpenID},
  203. }
  204. var resp wxPayTxnResp
  205. if err := c.postJSON("/v3/pay/transactions/jsapi", requestBody, &resp); err != nil {
  206. return nil, err
  207. }
  208. if resp.PrepayID == "" {
  209. return nil, errors.New("wxpay jsapi response missing prepay_id")
  210. }
  211. timeStamp := strconv.FormatInt(time.Now().Unix(), 10)
  212. nonceStr := randomString(32)
  213. pkg := "prepay_id=" + resp.PrepayID
  214. signPayload := appID + "\n" + timeStamp + "\n" + nonceStr + "\n" + pkg + "\n"
  215. paySign, err := c.signBase64(signPayload)
  216. if err != nil {
  217. return nil, err
  218. }
  219. return &WxMiniProgramPayResult{
  220. AppID: appID,
  221. TimeStamp: timeStamp,
  222. NonceStr: nonceStr,
  223. Package: pkg,
  224. SignType: "RSA",
  225. PaySign: paySign,
  226. PrepayID: resp.PrepayID,
  227. }, nil
  228. }
  229. func (c *WxPayClient) CreateH5Pay(req WxH5OrderReq) (*WxH5PayResult, error) {
  230. appID := req.AppID
  231. if appID == "" {
  232. appID = c.H5AppID
  233. }
  234. if appID == "" {
  235. return nil, errors.New("h5 appid is required")
  236. }
  237. if req.OutTradeNo == "" || req.Description == "" || req.Total <= 0 {
  238. return nil, errors.New("description/outTradeNo/total are required")
  239. }
  240. if req.PayerClientIP == "" {
  241. return nil, errors.New("payer_client_ip is required for h5 pay")
  242. }
  243. requestBody := wxPayTxnRequest{
  244. AppID: appID,
  245. MchID: c.MchID,
  246. Description: req.Description,
  247. OutTradeNo: req.OutTradeNo,
  248. NotifyURL: firstNotEmpty(req.NotifyURL, c.NotifyURL),
  249. Attach: req.Attach,
  250. Amount: WxPayAmount{
  251. Total: req.Total,
  252. Currency: firstNotEmpty(req.Currency, "CNY"),
  253. },
  254. SceneInfo: &WxPaySceneInfo{
  255. PayerClientIP: req.PayerClientIP,
  256. H5Info: &WxPayH5Info{Type: "Wap"},
  257. },
  258. }
  259. var resp wxPayTxnResp
  260. if err := c.postJSON("/v3/pay/transactions/h5", requestBody, &resp); err != nil {
  261. return nil, err
  262. }
  263. if resp.H5URL == "" {
  264. return nil, errors.New("wxpay h5 response missing h5_url")
  265. }
  266. return &WxH5PayResult{H5URL: resp.H5URL}, nil
  267. }
  268. func (c *WxPayClient) CreateAppPay(req WxAppOrderReq) (*WxAppPayResult, error) {
  269. appID := req.AppID
  270. if appID == "" {
  271. appID = c.AppAppID
  272. }
  273. if appID == "" {
  274. return nil, errors.New("app appid is required")
  275. }
  276. if req.OutTradeNo == "" || req.Description == "" || req.Total <= 0 {
  277. return nil, errors.New("description/outTradeNo/total are required")
  278. }
  279. requestBody := wxPayTxnRequest{
  280. AppID: appID,
  281. MchID: c.MchID,
  282. Description: req.Description,
  283. OutTradeNo: req.OutTradeNo,
  284. NotifyURL: firstNotEmpty(req.NotifyURL, c.NotifyURL),
  285. Attach: req.Attach,
  286. Amount: WxPayAmount{
  287. Total: req.Total,
  288. Currency: firstNotEmpty(req.Currency, "CNY"),
  289. },
  290. }
  291. var resp wxPayTxnResp
  292. if err := c.postJSON("/v3/pay/transactions/app", requestBody, &resp); err != nil {
  293. return nil, err
  294. }
  295. if resp.PrepayID == "" {
  296. return nil, errors.New("wxpay app response missing prepay_id")
  297. }
  298. timeStamp := strconv.FormatInt(time.Now().Unix(), 10)
  299. nonceStr := randomString(32)
  300. signPayload := appID + "\n" + timeStamp + "\n" + nonceStr + "\n" + resp.PrepayID + "\n"
  301. sign, err := c.signBase64(signPayload)
  302. if err != nil {
  303. return nil, err
  304. }
  305. return &WxAppPayResult{
  306. AppID: appID,
  307. PartnerID: c.MchID,
  308. PrepayID: resp.PrepayID,
  309. Package: "Sign=WXPay",
  310. NonceStr: nonceStr,
  311. TimeStamp: timeStamp,
  312. Sign: sign,
  313. }, nil
  314. }
  315. func (c *WxPayClient) postJSON(path string, payload interface{}, out interface{}) error {
  316. bodyBytes, err := json.Marshal(payload)
  317. if err != nil {
  318. return err
  319. }
  320. fullURL := c.APIBase + path
  321. auth, err := c.buildAuthorization("POST", fullURL, string(bodyBytes))
  322. if err != nil {
  323. return err
  324. }
  325. req, err := http.NewRequest(http.MethodPost, fullURL, bytes.NewReader(bodyBytes))
  326. if err != nil {
  327. return err
  328. }
  329. req.Header.Set("Content-Type", "application/json")
  330. req.Header.Set("Accept", "application/json")
  331. req.Header.Set("Authorization", auth)
  332. req.Header.Set("User-Agent", c.UserAgent)
  333. resp, err := c.HTTPClient.Do(req)
  334. if err != nil {
  335. return err
  336. }
  337. defer resp.Body.Close()
  338. respBody, err := io.ReadAll(resp.Body)
  339. if err != nil {
  340. return err
  341. }
  342. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  343. return fmt.Errorf("wxpay request failed: status=%d body=%s", resp.StatusCode, string(respBody))
  344. }
  345. if out == nil {
  346. return nil
  347. }
  348. if err := json.Unmarshal(respBody, out); err != nil {
  349. return fmt.Errorf("parse wxpay response failed: %w, body=%s", err, string(respBody))
  350. }
  351. return nil
  352. }
  353. func (c *WxPayClient) buildAuthorization(method, fullURL, body string) (string, error) {
  354. u, err := url.Parse(fullURL)
  355. if err != nil {
  356. return "", err
  357. }
  358. canonicalURL := u.Path
  359. if u.RawQuery != "" {
  360. canonicalURL += "?" + u.RawQuery
  361. }
  362. nonce := randomString(32)
  363. timestamp := strconv.FormatInt(time.Now().Unix(), 10)
  364. message := method + "\n" + canonicalURL + "\n" + timestamp + "\n" + nonce + "\n" + body + "\n"
  365. signature, err := c.signBase64(message)
  366. if err != nil {
  367. return "", err
  368. }
  369. token := fmt.Sprintf(`mchid="%s",nonce_str="%s",timestamp="%s",serial_no="%s",signature="%s"`,
  370. c.MchID, nonce, timestamp, c.SerialNo, signature,
  371. )
  372. return "WECHATPAY2-SHA256-RSA2048 " + token, nil
  373. }
  374. func (c *WxPayClient) signBase64(message string) (string, error) {
  375. sum := sha256.Sum256([]byte(message))
  376. sign, err := rsa.SignPKCS1v15(rand.Reader, c.PrivateKey, crypto.SHA256, sum[:])
  377. if err != nil {
  378. return "", err
  379. }
  380. return base64.StdEncoding.EncodeToString(sign), nil
  381. }
  382. func loadRSAPrivateKey(path string) (*rsa.PrivateKey, error) {
  383. raw, err := os.ReadFile(path)
  384. if err != nil {
  385. return nil, err
  386. }
  387. block, _ := pem.Decode(raw)
  388. if block == nil {
  389. return nil, errors.New("invalid private key pem")
  390. }
  391. if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
  392. if rsaKey, ok := key.(*rsa.PrivateKey); ok {
  393. return rsaKey, nil
  394. }
  395. }
  396. if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
  397. return key, nil
  398. }
  399. return nil, errors.New("unsupported private key format")
  400. }
  401. func randomString(n int) string {
  402. const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  403. if n <= 0 {
  404. return ""
  405. }
  406. b := make([]byte, n)
  407. rb := make([]byte, n)
  408. if _, err := rand.Read(rb); err != nil {
  409. return strconv.FormatInt(time.Now().UnixNano(), 10)
  410. }
  411. for i := 0; i < n; i++ {
  412. b[i] = chars[int(rb[i])%len(chars)]
  413. }
  414. return string(b)
  415. }
  416. func firstNotEmpty(values ...string) string {
  417. for _, v := range values {
  418. if strings.TrimSpace(v) != "" {
  419. return strings.TrimSpace(v)
  420. }
  421. }
  422. return ""
  423. }