easypay.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. // Package provider contains concrete payment provider implementations.
  2. package provider
  3. import (
  4. "context"
  5. "crypto/hmac"
  6. "crypto/md5"
  7. "encoding/hex"
  8. "encoding/json"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "net/url"
  13. "sort"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "unicode/utf8"
  18. "github.com/Wei-Shaw/sub2api/internal/payment"
  19. )
  20. // EasyPay constants.
  21. const (
  22. easypayCodeSuccess = 1
  23. easypayStatusPaid = 1
  24. easypayHTTPTimeout = 10 * time.Second
  25. maxEasypayResponseSize = 1 << 20 // 1MB
  26. maxEasypayErrorSummary = 512
  27. tradeStatusSuccess = "TRADE_SUCCESS"
  28. signTypeMD5 = "MD5"
  29. paymentModePopup = "popup"
  30. deviceMobile = "mobile"
  31. )
  32. // EasyPay implements payment.Provider for the EasyPay aggregation platform.
  33. type EasyPay struct {
  34. instanceID string
  35. config map[string]string
  36. httpClient *http.Client
  37. }
  38. type easyPayCustomMethod struct {
  39. Type string `json:"type"`
  40. UpstreamType string `json:"upstreamType"`
  41. DisplayName string `json:"displayName"`
  42. }
  43. // NewEasyPay creates a new EasyPay provider.
  44. // config keys: pid, pkey, apiBase, notifyUrl, returnUrl, cid, cidAlipay, cidWxpay
  45. func NewEasyPay(instanceID string, config map[string]string) (*EasyPay, error) {
  46. for _, k := range []string{"pid", "pkey", "apiBase", "notifyUrl", "returnUrl"} {
  47. if strings.TrimSpace(config[k]) == "" {
  48. return nil, fmt.Errorf("easypay config missing required key: %s", k)
  49. }
  50. }
  51. cfg := make(map[string]string, len(config))
  52. for k, v := range config {
  53. cfg[k] = v
  54. }
  55. cfg["apiBase"] = normalizeEasyPayAPIBase(cfg["apiBase"])
  56. return &EasyPay{
  57. instanceID: instanceID,
  58. config: cfg,
  59. httpClient: &http.Client{Timeout: easypayHTTPTimeout},
  60. }, nil
  61. }
  62. func normalizeEasyPayAPIBase(apiBase string) string {
  63. base := strings.TrimSpace(apiBase)
  64. if base == "" {
  65. return ""
  66. }
  67. if parsed, err := url.Parse(base); err == nil && parsed.Scheme != "" && parsed.Host != "" {
  68. parsed.RawQuery = ""
  69. parsed.Fragment = ""
  70. parsed.RawPath = ""
  71. parsed.Path = trimEasyPayEndpointPath(parsed.Path)
  72. return strings.TrimRight(parsed.String(), "/")
  73. }
  74. return strings.TrimRight(trimEasyPayEndpointPath(base), "/")
  75. }
  76. func trimEasyPayEndpointPath(path string) string {
  77. path = strings.TrimRight(strings.TrimSpace(path), "/")
  78. lower := strings.ToLower(path)
  79. for _, endpoint := range []string{"/submit.php", "/mapi.php", "/api.php"} {
  80. if strings.HasSuffix(lower, endpoint) {
  81. return strings.TrimRight(path[:len(path)-len(endpoint)], "/")
  82. }
  83. }
  84. return path
  85. }
  86. func (e *EasyPay) apiBase() string {
  87. if e == nil {
  88. return ""
  89. }
  90. return normalizeEasyPayAPIBase(e.config["apiBase"])
  91. }
  92. func (e *EasyPay) Name() string { return "EasyPay" }
  93. func (e *EasyPay) ProviderKey() string { return payment.TypeEasyPay }
  94. func (e *EasyPay) SupportedTypes() []payment.PaymentType {
  95. types := []payment.PaymentType{payment.TypeAlipay, payment.TypeWxpay}
  96. for _, method := range e.customMethods() {
  97. if method.Type != "" {
  98. types = append(types, method.Type)
  99. }
  100. }
  101. return types
  102. }
  103. func (e *EasyPay) MerchantIdentityMetadata() map[string]string {
  104. if e == nil {
  105. return nil
  106. }
  107. pid := strings.TrimSpace(e.config["pid"])
  108. if pid == "" {
  109. return nil
  110. }
  111. return map[string]string{"pid": pid}
  112. }
  113. func (e *EasyPay) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
  114. // Payment mode determined by instance config, not payment type.
  115. // "popup" → hosted page (submit.php); "qrcode"/default → API call (mapi.php).
  116. mode := e.config["paymentMode"]
  117. if mode == paymentModePopup {
  118. return e.createRedirectPayment(req)
  119. }
  120. return e.createAPIPayment(ctx, req)
  121. }
  122. // createRedirectPayment builds a submit.php URL for browser redirect.
  123. // No server-side API call — the user is redirected to EasyPay's hosted page.
  124. // TradeNo is empty; it arrives via the notify callback after payment.
  125. func (e *EasyPay) createRedirectPayment(req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
  126. notifyURL, returnURL := e.resolveURLs(req)
  127. paymentType := e.upstreamPaymentType(req.PaymentType)
  128. params := map[string]string{
  129. "pid": e.config["pid"], "type": paymentType,
  130. "out_trade_no": req.OrderID, "notify_url": notifyURL,
  131. "return_url": returnURL, "name": req.Subject,
  132. "money": req.Amount,
  133. }
  134. if cid := e.resolveCID(paymentType); cid != "" {
  135. params["cid"] = cid
  136. }
  137. if req.IsMobile {
  138. params["device"] = deviceMobile
  139. }
  140. params["sign"] = easyPaySign(params, e.config["pkey"])
  141. params["sign_type"] = signTypeMD5
  142. q := url.Values{}
  143. for k, v := range params {
  144. q.Set(k, v)
  145. }
  146. payURL := e.apiBase() + "/submit.php?" + q.Encode()
  147. return &payment.CreatePaymentResponse{PayURL: payURL}, nil
  148. }
  149. // createAPIPayment calls mapi.php to get payurl/qrcode (existing behavior).
  150. func (e *EasyPay) createAPIPayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
  151. notifyURL, returnURL := e.resolveURLs(req)
  152. paymentType := e.upstreamPaymentType(req.PaymentType)
  153. params := map[string]string{
  154. "pid": e.config["pid"], "type": paymentType,
  155. "out_trade_no": req.OrderID, "notify_url": notifyURL,
  156. "return_url": returnURL, "name": req.Subject,
  157. "money": req.Amount, "clientip": req.ClientIP,
  158. }
  159. if cid := e.resolveCID(paymentType); cid != "" {
  160. params["cid"] = cid
  161. }
  162. if req.IsMobile {
  163. params["device"] = deviceMobile
  164. }
  165. params["sign"] = easyPaySign(params, e.config["pkey"])
  166. params["sign_type"] = signTypeMD5
  167. body, err := e.post(ctx, e.apiBase()+"/mapi.php", params)
  168. if err != nil {
  169. return nil, fmt.Errorf("easypay create: %w", err)
  170. }
  171. var resp struct {
  172. Code int `json:"code"`
  173. Msg string `json:"msg"`
  174. TradeNo string `json:"trade_no"`
  175. PayURL string `json:"payurl"`
  176. PayURL2 string `json:"payurl2"` // H5 mobile payment URL
  177. QRCode string `json:"qrcode"`
  178. }
  179. if err := json.Unmarshal(body, &resp); err != nil {
  180. return nil, fmt.Errorf("easypay parse: %w", err)
  181. }
  182. if resp.Code != easypayCodeSuccess {
  183. return nil, fmt.Errorf("easypay error: %s", resp.Msg)
  184. }
  185. payURL := resp.PayURL
  186. if req.IsMobile && resp.PayURL2 != "" {
  187. payURL = resp.PayURL2
  188. }
  189. base := e.apiBase()
  190. return &payment.CreatePaymentResponse{
  191. TradeNo: resp.TradeNo,
  192. PayURL: resolveEasyPayReturnedRef(base, payURL),
  193. QRCode: resolveEasyPayReturnedRef(base, resp.QRCode),
  194. }, nil
  195. }
  196. // resolveEasyPayReturnedRef absolutizes a payurl/payurl2/qrcode reference that
  197. // mapi.php returned, resolving it against the instance's configured apiBase.
  198. //
  199. // EasyPay-compatible upstreams disagree on this field: some answer with a full
  200. // checkout URL, others with a site-root-relative path such as
  201. // "/api/pay/toapp/<order>". createRedirectPayment already builds an absolute URL
  202. // itself (apiBase + "/submit.php?..."), but the mapi.php branch stored whatever
  203. // came back verbatim, and nothing downstream repairs it —
  204. // sanitizeCreatePaymentResponseDetails only strips NUL bytes before the value is
  205. // persisted to pay_url/qr_code. The frontend then feeds qr_code straight into
  206. // QRCode.toCanvas (PaymentQRCodeView.renderQR), so a relative path becomes a QR
  207. // whose payload is a bare path: WeChat renders it as text, and pay_url resolves
  208. // against the gateway's own domain and 404s.
  209. //
  210. // That is precisely the outcome the Alipay provider already refuses to produce —
  211. // "Setting it as QRCode would let the frontend render an unscannable image"
  212. // (createPagePayTrade) — so EasyPay is brought in line with the same rule.
  213. //
  214. // Only references beginning with "/" are resolved. Anything carrying a scheme is
  215. // already actionable and is returned untouched, which covers both absolute
  216. // https:// checkout URLs and app deep links (weixin://, wxp://, alipays://). A
  217. // reference without a leading slash is left alone as well: QR payloads are
  218. // frequently opaque tokens rather than paths, and rewriting "OrderToken123" into
  219. // "<apiBase>/OrderToken123" would break a payload that works today — strictly
  220. // worse than the bug this fixes.
  221. func resolveEasyPayReturnedRef(apiBase, ref string) string {
  222. trimmed := strings.TrimSpace(ref)
  223. if !strings.HasPrefix(trimmed, "/") {
  224. return ref
  225. }
  226. base, err := url.Parse(strings.TrimSpace(apiBase))
  227. if err != nil || base.Scheme == "" || base.Host == "" {
  228. return ref
  229. }
  230. parsed, err := url.Parse(trimmed)
  231. if err != nil || parsed.Scheme != "" {
  232. return ref
  233. }
  234. return base.ResolveReference(parsed).String()
  235. }
  236. // resolveURLs returns (notifyURL, returnURL) preferring request values,
  237. // falling back to instance config.
  238. func (e *EasyPay) resolveURLs(req payment.CreatePaymentRequest) (string, string) {
  239. notifyURL := req.NotifyURL
  240. if notifyURL == "" {
  241. notifyURL = e.config["notifyUrl"]
  242. }
  243. returnURL := req.ReturnURL
  244. if returnURL == "" {
  245. returnURL = e.config["returnUrl"]
  246. }
  247. return notifyURL, returnURL
  248. }
  249. func (e *EasyPay) customMethods() []easyPayCustomMethod {
  250. if e == nil {
  251. return nil
  252. }
  253. raw := strings.TrimSpace(e.config["customMethods"])
  254. if raw == "" {
  255. return nil
  256. }
  257. var methods []easyPayCustomMethod
  258. if err := json.Unmarshal([]byte(raw), &methods); err != nil {
  259. return nil
  260. }
  261. result := make([]easyPayCustomMethod, 0, len(methods))
  262. for _, method := range methods {
  263. method.Type = strings.TrimSpace(method.Type)
  264. method.UpstreamType = strings.TrimSpace(method.UpstreamType)
  265. method.DisplayName = strings.TrimSpace(method.DisplayName)
  266. if method.Type == "" || method.UpstreamType == "" {
  267. continue
  268. }
  269. result = append(result, method)
  270. }
  271. return result
  272. }
  273. func (e *EasyPay) upstreamPaymentType(paymentType string) string {
  274. paymentType = strings.TrimSpace(paymentType)
  275. for _, method := range e.customMethods() {
  276. if paymentType == method.Type {
  277. return method.UpstreamType
  278. }
  279. }
  280. return paymentType
  281. }
  282. func (e *EasyPay) QueryOrder(ctx context.Context, tradeNo string) (*payment.QueryOrderResponse, error) {
  283. params := map[string]string{
  284. "act": "order", "pid": e.config["pid"],
  285. "key": e.config["pkey"], "out_trade_no": tradeNo,
  286. }
  287. body, err := e.post(ctx, e.apiBase()+"/api.php", params)
  288. if err != nil {
  289. return nil, fmt.Errorf("easypay query: %w", err)
  290. }
  291. type easyPayQueryData struct {
  292. TradeStatus *string `json:"trade_status"`
  293. Status *int `json:"status"`
  294. Money *string `json:"money"`
  295. TradeNo *string `json:"trade_no"`
  296. }
  297. var resp struct {
  298. Code int `json:"code"`
  299. Msg string `json:"msg"`
  300. TradeStatus *string `json:"trade_status"`
  301. Status *int `json:"status"`
  302. Money *string `json:"money"`
  303. TradeNo *string `json:"trade_no"`
  304. Data easyPayQueryData `json:"data"`
  305. }
  306. if err := json.Unmarshal(body, &resp); err != nil {
  307. return nil, fmt.Errorf("easypay parse query: %w", err)
  308. }
  309. status := payment.ProviderStatusPending
  310. if resp.TradeStatus != nil {
  311. if *resp.TradeStatus == tradeStatusSuccess {
  312. status = payment.ProviderStatusPaid
  313. }
  314. } else if resp.Data.TradeStatus != nil {
  315. if *resp.Data.TradeStatus == tradeStatusSuccess {
  316. status = payment.ProviderStatusPaid
  317. }
  318. } else if resp.Status != nil {
  319. if *resp.Status == easypayStatusPaid {
  320. status = payment.ProviderStatusPaid
  321. }
  322. } else if resp.Data.Status != nil && *resp.Data.Status == easypayStatusPaid {
  323. status = payment.ProviderStatusPaid
  324. }
  325. money := ""
  326. if resp.Money != nil {
  327. money = *resp.Money
  328. } else if resp.Data.Money != nil {
  329. money = *resp.Data.Money
  330. }
  331. responseTradeNo := tradeNo
  332. if resp.TradeNo != nil {
  333. if *resp.TradeNo != "" {
  334. responseTradeNo = *resp.TradeNo
  335. }
  336. } else if resp.Data.TradeNo != nil && *resp.Data.TradeNo != "" {
  337. responseTradeNo = *resp.Data.TradeNo
  338. }
  339. amount, _ := strconv.ParseFloat(money, 64)
  340. return &payment.QueryOrderResponse{
  341. TradeNo: responseTradeNo,
  342. Status: status,
  343. Amount: amount,
  344. Metadata: e.MerchantIdentityMetadata(),
  345. }, nil
  346. }
  347. func (e *EasyPay) VerifyNotification(_ context.Context, rawBody string, _ map[string]string) (*payment.PaymentNotification, error) {
  348. values, err := url.ParseQuery(rawBody)
  349. if err != nil {
  350. return nil, fmt.Errorf("parse notify: %w", err)
  351. }
  352. // url.ParseQuery already decodes values — no additional decode needed.
  353. params := make(map[string]string)
  354. for k := range values {
  355. params[k] = values.Get(k)
  356. }
  357. sign := params["sign"]
  358. if sign == "" {
  359. return nil, fmt.Errorf("missing sign")
  360. }
  361. if !easyPayVerifySign(params, e.config["pkey"], sign) {
  362. return nil, fmt.Errorf("invalid signature")
  363. }
  364. status := payment.ProviderStatusFailed
  365. if params["trade_status"] == tradeStatusSuccess {
  366. status = payment.ProviderStatusSuccess
  367. }
  368. amount, _ := strconv.ParseFloat(params["money"], 64)
  369. metadata := e.MerchantIdentityMetadata()
  370. if pid := strings.TrimSpace(params["pid"]); pid != "" {
  371. if metadata == nil {
  372. metadata = map[string]string{}
  373. }
  374. metadata["pid"] = pid
  375. }
  376. return &payment.PaymentNotification{
  377. TradeNo: params["trade_no"], OrderID: params["out_trade_no"],
  378. Amount: amount, Status: status, RawData: rawBody, Metadata: metadata,
  379. }, nil
  380. }
  381. func (e *EasyPay) Refund(ctx context.Context, req payment.RefundRequest) (*payment.RefundResponse, error) {
  382. attempts := e.refundAttempts(req)
  383. if len(attempts) == 0 {
  384. return nil, fmt.Errorf("easypay refund missing order identifier")
  385. }
  386. var firstErr error
  387. for i, attempt := range attempts {
  388. body, status, err := e.postRaw(ctx, e.apiBase()+"/api.php?act=refund", attempt.params)
  389. if err != nil {
  390. return nil, fmt.Errorf("easypay refund request: %w", err)
  391. }
  392. if err := parseEasyPayRefundResponse(status, body); err != nil {
  393. if firstErr == nil {
  394. firstErr = err
  395. }
  396. if i+1 < len(attempts) && isEasyPayRefundOrderNotFound(err) {
  397. continue
  398. }
  399. return nil, err
  400. }
  401. return &payment.RefundResponse{RefundID: attempt.refundID, Status: payment.ProviderStatusSuccess}, nil
  402. }
  403. return nil, firstErr
  404. }
  405. type easyPayRefundAttempt struct {
  406. params map[string]string
  407. refundID string
  408. }
  409. func (e *EasyPay) refundAttempts(req payment.RefundRequest) []easyPayRefundAttempt {
  410. base := map[string]string{
  411. "pid": e.config["pid"], "key": e.config["pkey"], "money": req.Amount,
  412. }
  413. var attempts []easyPayRefundAttempt
  414. if orderID := strings.TrimSpace(req.OrderID); orderID != "" {
  415. params := cloneStringMap(base)
  416. params["out_trade_no"] = orderID
  417. attempts = append(attempts, easyPayRefundAttempt{params: params, refundID: orderID})
  418. }
  419. if tradeNo := strings.TrimSpace(req.TradeNo); tradeNo != "" {
  420. params := cloneStringMap(base)
  421. params["trade_no"] = tradeNo
  422. attempts = append(attempts, easyPayRefundAttempt{params: params, refundID: tradeNo})
  423. }
  424. return attempts
  425. }
  426. func cloneStringMap(in map[string]string) map[string]string {
  427. out := make(map[string]string, len(in))
  428. for k, v := range in {
  429. out[k] = v
  430. }
  431. return out
  432. }
  433. func isEasyPayRefundOrderNotFound(err error) bool {
  434. if err == nil {
  435. return false
  436. }
  437. msg := err.Error()
  438. lower := strings.ToLower(msg)
  439. return strings.Contains(msg, "订单编号不存在") ||
  440. strings.Contains(msg, "订单不存在") ||
  441. strings.Contains(lower, "order not found") ||
  442. strings.Contains(lower, "not exist")
  443. }
  444. func parseEasyPayRefundResponse(status int, body []byte) error {
  445. summary := summarizeEasyPayResponse(body)
  446. if status < http.StatusOK || status >= http.StatusMultipleChoices {
  447. return fmt.Errorf("easypay refund HTTP %d: %s", status, summary)
  448. }
  449. trimmed := strings.TrimSpace(string(body))
  450. if trimmed == "" {
  451. return fmt.Errorf("easypay refund empty response (HTTP %d): %s", status, summary)
  452. }
  453. lower := strings.ToLower(trimmed)
  454. if strings.HasPrefix(lower, "<!doctype html") || strings.HasPrefix(lower, "<html") ||
  455. (strings.HasPrefix(lower, "<") && strings.Contains(lower, "html")) {
  456. return fmt.Errorf("easypay refund non-JSON response (HTTP %d): %s", status, summary)
  457. }
  458. var resp struct {
  459. Code any `json:"code"`
  460. Msg string `json:"msg"`
  461. }
  462. if err := json.Unmarshal(body, &resp); err != nil {
  463. return fmt.Errorf("easypay refund non-JSON response (HTTP %d): %s", status, summary)
  464. }
  465. if !easyPayResponseCodeIsSuccess(resp.Code) {
  466. msg := strings.TrimSpace(resp.Msg)
  467. if msg == "" {
  468. msg = summary
  469. }
  470. return fmt.Errorf("easypay refund failed (HTTP %d): %s", status, msg)
  471. }
  472. return nil
  473. }
  474. func easyPayResponseCodeIsSuccess(code any) bool {
  475. switch v := code.(type) {
  476. case float64:
  477. return int(v) == easypayCodeSuccess
  478. case string:
  479. n, err := strconv.Atoi(strings.TrimSpace(v))
  480. return err == nil && n == easypayCodeSuccess
  481. default:
  482. return false
  483. }
  484. }
  485. func summarizeEasyPayResponse(body []byte) string {
  486. summary := strings.Join(strings.Fields(string(body)), " ")
  487. if summary == "" {
  488. return "<empty>"
  489. }
  490. if len(summary) > maxEasypayErrorSummary {
  491. truncated := summary[:maxEasypayErrorSummary]
  492. for len(truncated) > 0 && !utf8.ValidString(truncated) {
  493. truncated = truncated[:len(truncated)-1]
  494. }
  495. return truncated + "..."
  496. }
  497. return summary
  498. }
  499. func (e *EasyPay) resolveCID(paymentType string) string {
  500. if strings.HasPrefix(paymentType, "alipay") {
  501. if v := e.config["cidAlipay"]; v != "" {
  502. return v
  503. }
  504. return e.config["cid"]
  505. }
  506. if v := e.config["cidWxpay"]; v != "" {
  507. return v
  508. }
  509. return e.config["cid"]
  510. }
  511. func (e *EasyPay) post(ctx context.Context, endpoint string, params map[string]string) ([]byte, error) {
  512. body, _, err := e.postRaw(ctx, endpoint, params)
  513. return body, err
  514. }
  515. func (e *EasyPay) postRaw(ctx context.Context, endpoint string, params map[string]string) ([]byte, int, error) {
  516. form := url.Values{}
  517. for k, v := range params {
  518. form.Set(k, v)
  519. }
  520. req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode()))
  521. if err != nil {
  522. return nil, 0, err
  523. }
  524. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  525. client := e.httpClient
  526. if client == nil {
  527. client = &http.Client{Timeout: easypayHTTPTimeout}
  528. }
  529. resp, err := client.Do(req)
  530. if err != nil {
  531. return nil, 0, err
  532. }
  533. defer func() { _ = resp.Body.Close() }()
  534. body, err := io.ReadAll(io.LimitReader(resp.Body, maxEasypayResponseSize))
  535. if err != nil {
  536. return nil, resp.StatusCode, err
  537. }
  538. return body, resp.StatusCode, nil
  539. }
  540. func easyPaySign(params map[string]string, pkey string) string {
  541. keys := make([]string, 0, len(params))
  542. for k, v := range params {
  543. if k == "sign" || k == "sign_type" || v == "" {
  544. continue
  545. }
  546. keys = append(keys, k)
  547. }
  548. sort.Strings(keys)
  549. var buf strings.Builder
  550. for i, k := range keys {
  551. if i > 0 {
  552. _ = buf.WriteByte('&')
  553. }
  554. _, _ = buf.WriteString(k + "=" + params[k])
  555. }
  556. _, _ = buf.WriteString(pkey)
  557. hash := md5.Sum([]byte(buf.String()))
  558. return hex.EncodeToString(hash[:])
  559. }
  560. func easyPayVerifySign(params map[string]string, pkey string, sign string) bool {
  561. return hmac.Equal([]byte(easyPaySign(params, pkey)), []byte(sign))
  562. }