wxpay.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. package provider
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "net/url"
  9. "strconv"
  10. "strings"
  11. "sync"
  12. "github.com/Wei-Shaw/sub2api/internal/payment"
  13. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  14. "github.com/wechatpay-apiv3/wechatpay-go/core"
  15. "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
  16. "github.com/wechatpay-apiv3/wechatpay-go/core/notify"
  17. "github.com/wechatpay-apiv3/wechatpay-go/core/option"
  18. "github.com/wechatpay-apiv3/wechatpay-go/services/payments"
  19. "github.com/wechatpay-apiv3/wechatpay-go/services/payments/h5"
  20. "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
  21. "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
  22. "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
  23. "github.com/wechatpay-apiv3/wechatpay-go/utils"
  24. )
  25. // WeChat Pay constants.
  26. const (
  27. wxpayCurrency = "CNY"
  28. wxpayH5Type = "Wap"
  29. wxpayResultPath = "/payment/result"
  30. )
  31. const (
  32. wxpayMetadataAppID = "appid"
  33. wxpayMetadataMerchantID = "mchid"
  34. wxpayMetadataCurrency = "currency"
  35. wxpayMetadataTradeState = "trade_state"
  36. )
  37. // WeChat Pay create-payment modes.
  38. const (
  39. wxpayModeNative = "native"
  40. wxpayModeH5 = "h5"
  41. wxpayModeJSAPI = "jsapi"
  42. )
  43. // WeChat Pay trade states.
  44. const (
  45. wxpayTradeStateSuccess = "SUCCESS"
  46. wxpayTradeStateRefund = "REFUND"
  47. wxpayTradeStateClosed = "CLOSED"
  48. wxpayTradeStatePayError = "PAYERROR"
  49. )
  50. // WeChat Pay notification event types.
  51. const (
  52. wxpayEventTransactionSuccess = "TRANSACTION.SUCCESS"
  53. )
  54. var (
  55. wxpayNativePrepay = func(ctx context.Context, svc native.NativeApiService, req native.PrepayRequest) (*native.PrepayResponse, *core.APIResult, error) {
  56. return svc.Prepay(ctx, req)
  57. }
  58. wxpayH5Prepay = func(ctx context.Context, svc h5.H5ApiService, req h5.PrepayRequest) (*h5.PrepayResponse, *core.APIResult, error) {
  59. return svc.Prepay(ctx, req)
  60. }
  61. wxpayJSAPIPrepayWithRequestPayment = func(ctx context.Context, svc jsapi.JsapiApiService, req jsapi.PrepayRequest) (*jsapi.PrepayWithRequestPaymentResponse, *core.APIResult, error) {
  62. return svc.PrepayWithRequestPayment(ctx, req)
  63. }
  64. )
  65. type Wxpay struct {
  66. instanceID string
  67. config map[string]string
  68. mu sync.Mutex
  69. coreClient *core.Client
  70. notifyHandler *notify.Handler
  71. }
  72. const wxpayAPIv3KeyLength = 32
  73. func NewWxpay(instanceID string, config map[string]string) (*Wxpay, error) {
  74. // All fields are required. Platform-certificate mode is intentionally unsupported —
  75. // WeChat has been migrating all merchants to the pubkey verifier since 2024-10,
  76. // and newly-provisioned merchants cannot download platform certificates at all.
  77. required := []string{"appId", "mchId", "privateKey", "apiV3Key", "certSerial", "publicKey", "publicKeyId"}
  78. for _, k := range required {
  79. if config[k] == "" {
  80. return nil, infraerrors.BadRequest("WXPAY_CONFIG_MISSING_KEY", "missing_required_key").
  81. WithMetadata(map[string]string{"key": k})
  82. }
  83. }
  84. if len(config["apiV3Key"]) != wxpayAPIv3KeyLength {
  85. return nil, infraerrors.BadRequest("WXPAY_CONFIG_INVALID_KEY_LENGTH", "invalid_key_length").
  86. WithMetadata(map[string]string{
  87. "key": "apiV3Key",
  88. "expected": strconv.Itoa(wxpayAPIv3KeyLength),
  89. "actual": strconv.Itoa(len(config["apiV3Key"])),
  90. })
  91. }
  92. // Parse PEMs eagerly so malformed keys surface at save time, not at order creation.
  93. if _, err := utils.LoadPrivateKey(formatPEM(config["privateKey"], "PRIVATE KEY")); err != nil {
  94. return nil, infraerrors.BadRequest("WXPAY_CONFIG_INVALID_KEY", "invalid_key").
  95. WithMetadata(map[string]string{"key": "privateKey"})
  96. }
  97. if _, err := utils.LoadPublicKey(formatPEM(config["publicKey"], "PUBLIC KEY")); err != nil {
  98. return nil, infraerrors.BadRequest("WXPAY_CONFIG_INVALID_KEY", "invalid_key").
  99. WithMetadata(map[string]string{"key": "publicKey"})
  100. }
  101. return &Wxpay{instanceID: instanceID, config: config}, nil
  102. }
  103. func (w *Wxpay) Name() string { return "Wxpay" }
  104. func (w *Wxpay) ProviderKey() string { return payment.TypeWxpay }
  105. func (w *Wxpay) SupportedTypes() []payment.PaymentType {
  106. return []payment.PaymentType{payment.TypeWxpay}
  107. }
  108. // ResolveWxpayJSAPIAppID returns the AppID that JSAPI prepay will use for a
  109. // given provider config. A dedicated MP AppID takes precedence over the base
  110. // merchant AppID.
  111. func ResolveWxpayJSAPIAppID(config map[string]string) string {
  112. if appID := strings.TrimSpace(config["mpAppId"]); appID != "" {
  113. return appID
  114. }
  115. return strings.TrimSpace(config["appId"])
  116. }
  117. func formatPEM(key, keyType string) string {
  118. key = strings.TrimSpace(key)
  119. if strings.HasPrefix(key, "-----BEGIN") {
  120. return key
  121. }
  122. return fmt.Sprintf("-----BEGIN %s-----\n%s\n-----END %s-----", keyType, key, keyType)
  123. }
  124. func (w *Wxpay) ensureClient() (*core.Client, error) {
  125. w.mu.Lock()
  126. defer w.mu.Unlock()
  127. if w.coreClient != nil {
  128. return w.coreClient, nil
  129. }
  130. privateKey, err := utils.LoadPrivateKey(formatPEM(w.config["privateKey"], "PRIVATE KEY"))
  131. if err != nil {
  132. return nil, infraerrors.BadRequest("WXPAY_CONFIG_INVALID_KEY", "invalid_key").
  133. WithMetadata(map[string]string{"key": "privateKey"})
  134. }
  135. publicKey, err := utils.LoadPublicKey(formatPEM(w.config["publicKey"], "PUBLIC KEY"))
  136. if err != nil {
  137. return nil, infraerrors.BadRequest("WXPAY_CONFIG_INVALID_KEY", "invalid_key").
  138. WithMetadata(map[string]string{"key": "publicKey"})
  139. }
  140. verifier := verifiers.NewSHA256WithRSAPubkeyVerifier(w.config["publicKeyId"], *publicKey)
  141. client, err := core.NewClient(context.Background(),
  142. option.WithMerchantCredential(w.config["mchId"], w.config["certSerial"], privateKey),
  143. option.WithVerifier(verifier))
  144. if err != nil {
  145. return nil, fmt.Errorf("wxpay init client: %w", err)
  146. }
  147. handler, err := notify.NewRSANotifyHandler(w.config["apiV3Key"], verifier)
  148. if err != nil {
  149. return nil, fmt.Errorf("wxpay init notify handler: %w", err)
  150. }
  151. w.notifyHandler = handler
  152. w.coreClient = client
  153. return w.coreClient, nil
  154. }
  155. func (w *Wxpay) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
  156. client, err := w.ensureClient()
  157. if err != nil {
  158. return nil, err
  159. }
  160. // Request-first, config-fallback (consistent with EasyPay/Alipay)
  161. notifyURL := req.NotifyURL
  162. if notifyURL == "" {
  163. notifyURL = w.config["notifyUrl"]
  164. }
  165. if notifyURL == "" {
  166. return nil, fmt.Errorf("wxpay notifyUrl is required")
  167. }
  168. totalFen, err := payment.YuanToFen(req.Amount)
  169. if err != nil {
  170. return nil, fmt.Errorf("wxpay create payment: %w", err)
  171. }
  172. mode, err := resolveWxpayCreateMode(req)
  173. if err != nil {
  174. return nil, err
  175. }
  176. switch mode {
  177. case wxpayModeJSAPI:
  178. return w.prepayJSAPI(ctx, client, req, notifyURL, totalFen)
  179. case wxpayModeH5:
  180. return w.prepayH5(ctx, client, req, notifyURL, totalFen)
  181. case wxpayModeNative:
  182. return w.prepayNative(ctx, client, req, notifyURL, totalFen)
  183. default:
  184. return nil, fmt.Errorf("wxpay create payment: unsupported mode %q", mode)
  185. }
  186. }
  187. func (w *Wxpay) prepayJSAPI(ctx context.Context, c *core.Client, req payment.CreatePaymentRequest, notifyURL string, totalFen int64) (*payment.CreatePaymentResponse, error) {
  188. svc := jsapi.JsapiApiService{Client: c}
  189. cur := wxpayCurrency
  190. appID := ResolveWxpayJSAPIAppID(w.config)
  191. prepayReq := jsapi.PrepayRequest{
  192. Appid: core.String(appID),
  193. Mchid: core.String(w.config["mchId"]),
  194. Description: core.String(req.Subject),
  195. OutTradeNo: core.String(req.OrderID),
  196. NotifyUrl: core.String(notifyURL),
  197. Amount: &jsapi.Amount{Total: core.Int64(totalFen), Currency: &cur},
  198. Payer: &jsapi.Payer{Openid: core.String(strings.TrimSpace(req.OpenID))},
  199. }
  200. if clientIP := strings.TrimSpace(req.ClientIP); clientIP != "" {
  201. prepayReq.SceneInfo = &jsapi.SceneInfo{PayerClientIp: core.String(clientIP)}
  202. }
  203. resp, _, err := wxpayJSAPIPrepayWithRequestPayment(ctx, svc, prepayReq)
  204. if err != nil {
  205. return nil, fmt.Errorf("wxpay jsapi prepay: %w", err)
  206. }
  207. return &payment.CreatePaymentResponse{
  208. TradeNo: req.OrderID,
  209. ResultType: payment.CreatePaymentResultJSAPIReady,
  210. JSAPI: &payment.WechatJSAPIPayload{
  211. AppID: wxSV(resp.Appid),
  212. TimeStamp: wxSV(resp.TimeStamp),
  213. NonceStr: wxSV(resp.NonceStr),
  214. Package: wxSV(resp.Package),
  215. SignType: wxSV(resp.SignType),
  216. PaySign: wxSV(resp.PaySign),
  217. },
  218. }, nil
  219. }
  220. func (w *Wxpay) prepayNative(ctx context.Context, c *core.Client, req payment.CreatePaymentRequest, notifyURL string, totalFen int64) (*payment.CreatePaymentResponse, error) {
  221. svc := native.NativeApiService{Client: c}
  222. cur := wxpayCurrency
  223. resp, _, err := wxpayNativePrepay(ctx, svc, native.PrepayRequest{
  224. Appid: core.String(w.config["appId"]), Mchid: core.String(w.config["mchId"]),
  225. Description: core.String(req.Subject), OutTradeNo: core.String(req.OrderID),
  226. NotifyUrl: core.String(notifyURL),
  227. Amount: &native.Amount{Total: core.Int64(totalFen), Currency: &cur},
  228. })
  229. if err != nil {
  230. return nil, fmt.Errorf("wxpay native prepay: %w", err)
  231. }
  232. codeURL := ""
  233. if resp.CodeUrl != nil {
  234. codeURL = *resp.CodeUrl
  235. }
  236. return &payment.CreatePaymentResponse{TradeNo: req.OrderID, QRCode: codeURL}, nil
  237. }
  238. func (w *Wxpay) prepayH5(ctx context.Context, c *core.Client, req payment.CreatePaymentRequest, notifyURL string, totalFen int64) (*payment.CreatePaymentResponse, error) {
  239. svc := h5.H5ApiService{Client: c}
  240. cur := wxpayCurrency
  241. resp, _, err := wxpayH5Prepay(ctx, svc, h5.PrepayRequest{
  242. Appid: core.String(w.config["appId"]), Mchid: core.String(w.config["mchId"]),
  243. Description: core.String(req.Subject), OutTradeNo: core.String(req.OrderID),
  244. NotifyUrl: core.String(notifyURL),
  245. Amount: &h5.Amount{Total: core.Int64(totalFen), Currency: &cur},
  246. SceneInfo: &h5.SceneInfo{PayerClientIp: core.String(req.ClientIP), H5Info: buildWxpayH5Info(w.config)},
  247. })
  248. if err != nil {
  249. return nil, fmt.Errorf("wxpay h5 prepay: %w", err)
  250. }
  251. h5URL := ""
  252. if resp.H5Url != nil {
  253. h5URL = *resp.H5Url
  254. }
  255. h5URL, err = appendWxpayRedirectURL(h5URL, req)
  256. if err != nil {
  257. return nil, err
  258. }
  259. return &payment.CreatePaymentResponse{TradeNo: req.OrderID, PayURL: h5URL}, nil
  260. }
  261. func buildWxpayH5Info(config map[string]string) *h5.H5Info {
  262. tp := wxpayH5Type
  263. info := &h5.H5Info{Type: &tp}
  264. if appName := strings.TrimSpace(config["h5AppName"]); appName != "" {
  265. info.AppName = core.String(appName)
  266. }
  267. if appURL := strings.TrimSpace(config["h5AppUrl"]); appURL != "" {
  268. info.AppUrl = core.String(appURL)
  269. }
  270. return info
  271. }
  272. func resolveWxpayCreateMode(req payment.CreatePaymentRequest) (string, error) {
  273. if strings.TrimSpace(req.OpenID) != "" {
  274. return wxpayModeJSAPI, nil
  275. }
  276. if req.IsMobile {
  277. if strings.TrimSpace(req.ClientIP) == "" {
  278. return "", fmt.Errorf("wxpay H5 payment requires client IP")
  279. }
  280. return wxpayModeH5, nil
  281. }
  282. return wxpayModeNative, nil
  283. }
  284. func appendWxpayRedirectURL(h5URL string, req payment.CreatePaymentRequest) (string, error) {
  285. h5URL = strings.TrimSpace(h5URL)
  286. returnURL := strings.TrimSpace(req.ReturnURL)
  287. if h5URL == "" || returnURL == "" {
  288. return h5URL, nil
  289. }
  290. redirectURL, err := buildWxpayResultURL(returnURL, req)
  291. if err != nil {
  292. return "", err
  293. }
  294. sep := "&"
  295. if !strings.Contains(h5URL, "?") {
  296. sep = "?"
  297. }
  298. return h5URL + sep + "redirect_url=" + url.QueryEscape(redirectURL), nil
  299. }
  300. func buildWxpayResultURL(returnURL string, req payment.CreatePaymentRequest) (string, error) {
  301. u, err := url.Parse(returnURL)
  302. if err != nil || !u.IsAbs() || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
  303. return "", fmt.Errorf("return URL must be an absolute http(s) URL")
  304. }
  305. values := u.Query()
  306. values.Set("out_trade_no", strings.TrimSpace(req.OrderID))
  307. if paymentType := strings.TrimSpace(req.PaymentType); paymentType != "" {
  308. values.Set("payment_type", paymentType)
  309. }
  310. if strings.TrimSpace(u.Path) == "" {
  311. u.Path = wxpayResultPath
  312. }
  313. u.RawPath = ""
  314. u.RawQuery = values.Encode()
  315. u.Fragment = ""
  316. return u.String(), nil
  317. }
  318. func wxSV(s *string) string {
  319. if s == nil {
  320. return ""
  321. }
  322. return *s
  323. }
  324. func mapWxState(s string) string {
  325. switch s {
  326. case wxpayTradeStateSuccess:
  327. return payment.ProviderStatusPaid
  328. case wxpayTradeStateRefund:
  329. return payment.ProviderStatusRefunded
  330. case wxpayTradeStateClosed, wxpayTradeStatePayError:
  331. return payment.ProviderStatusFailed
  332. default:
  333. return payment.ProviderStatusPending
  334. }
  335. }
  336. func buildWxpayTransactionMetadata(tx *payments.Transaction) map[string]string {
  337. if tx == nil {
  338. return nil
  339. }
  340. metadata := map[string]string{}
  341. if appID := wxSV(tx.Appid); appID != "" {
  342. metadata[wxpayMetadataAppID] = appID
  343. }
  344. if merchantID := wxSV(tx.Mchid); merchantID != "" {
  345. metadata[wxpayMetadataMerchantID] = merchantID
  346. }
  347. if tradeState := wxSV(tx.TradeState); tradeState != "" {
  348. metadata[wxpayMetadataTradeState] = tradeState
  349. }
  350. if tx.Amount != nil {
  351. if currency := wxSV(tx.Amount.Currency); currency != "" {
  352. metadata[wxpayMetadataCurrency] = currency
  353. }
  354. }
  355. if len(metadata) == 0 {
  356. return nil
  357. }
  358. return metadata
  359. }
  360. func (w *Wxpay) QueryOrder(ctx context.Context, tradeNo string) (*payment.QueryOrderResponse, error) {
  361. c, err := w.ensureClient()
  362. if err != nil {
  363. return nil, err
  364. }
  365. svc := native.NativeApiService{Client: c}
  366. tx, _, err := svc.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
  367. OutTradeNo: core.String(tradeNo), Mchid: core.String(w.config["mchId"]),
  368. })
  369. if err != nil {
  370. return nil, fmt.Errorf("wxpay query order: %w", err)
  371. }
  372. var amt float64
  373. if tx.Amount != nil && tx.Amount.Total != nil {
  374. amt = payment.FenToYuan(*tx.Amount.Total)
  375. }
  376. id := tradeNo
  377. if tx.TransactionId != nil {
  378. id = *tx.TransactionId
  379. }
  380. pa := ""
  381. if tx.SuccessTime != nil {
  382. pa = *tx.SuccessTime
  383. }
  384. return &payment.QueryOrderResponse{
  385. TradeNo: id,
  386. Status: mapWxState(wxSV(tx.TradeState)),
  387. Amount: amt,
  388. PaidAt: pa,
  389. Metadata: buildWxpayTransactionMetadata(tx),
  390. }, nil
  391. }
  392. func (w *Wxpay) VerifyNotification(ctx context.Context, rawBody string, headers map[string]string) (*payment.PaymentNotification, error) {
  393. if _, err := w.ensureClient(); err != nil {
  394. return nil, err
  395. }
  396. r, err := http.NewRequestWithContext(ctx, http.MethodPost, "/", io.NopCloser(bytes.NewBufferString(rawBody)))
  397. if err != nil {
  398. return nil, fmt.Errorf("wxpay construct request: %w", err)
  399. }
  400. for k, v := range headers {
  401. r.Header.Set(k, v)
  402. }
  403. var tx payments.Transaction
  404. nr, err := w.notifyHandler.ParseNotifyRequest(ctx, r, &tx)
  405. if err != nil {
  406. return nil, fmt.Errorf("wxpay verify notification: %w", err)
  407. }
  408. if nr.EventType != wxpayEventTransactionSuccess {
  409. return nil, nil
  410. }
  411. var amt float64
  412. if tx.Amount != nil && tx.Amount.Total != nil {
  413. amt = payment.FenToYuan(*tx.Amount.Total)
  414. }
  415. st := payment.ProviderStatusFailed
  416. if wxSV(tx.TradeState) == wxpayTradeStateSuccess {
  417. st = payment.ProviderStatusSuccess
  418. }
  419. return &payment.PaymentNotification{
  420. TradeNo: wxSV(tx.TransactionId), OrderID: wxSV(tx.OutTradeNo),
  421. Amount: amt, Status: st, RawData: rawBody, Metadata: buildWxpayTransactionMetadata(&tx),
  422. }, nil
  423. }
  424. func (w *Wxpay) Refund(ctx context.Context, req payment.RefundRequest) (*payment.RefundResponse, error) {
  425. c, err := w.ensureClient()
  426. if err != nil {
  427. return nil, err
  428. }
  429. rf, err := payment.YuanToFen(req.Amount)
  430. if err != nil {
  431. return nil, fmt.Errorf("wxpay refund amount: %w", err)
  432. }
  433. tf, err := w.queryOrderTotalFen(ctx, c, req.OrderID)
  434. if err != nil {
  435. return nil, err
  436. }
  437. rs := refunddomestic.RefundsApiService{Client: c}
  438. cur := wxpayCurrency
  439. outRefundNo := wxpayRefundID(req.OrderID, req.Amount)
  440. res, _, err := rs.Create(ctx, refunddomestic.CreateRequest{
  441. OutTradeNo: core.String(req.OrderID),
  442. OutRefundNo: core.String(outRefundNo),
  443. Reason: core.String(req.Reason),
  444. Amount: &refunddomestic.AmountReq{Refund: core.Int64(rf), Total: core.Int64(tf), Currency: &cur},
  445. })
  446. if err != nil {
  447. return nil, fmt.Errorf("wxpay refund: %w", err)
  448. }
  449. st := payment.ProviderStatusPending
  450. if res.Status != nil && *res.Status == refunddomestic.STATUS_SUCCESS {
  451. st = payment.ProviderStatusSuccess
  452. }
  453. return &payment.RefundResponse{RefundID: outRefundNo, Status: st}, nil
  454. }
  455. func (w *Wxpay) QueryRefund(ctx context.Context, req payment.RefundQueryRequest) (*payment.RefundResponse, error) {
  456. c, err := w.ensureClient()
  457. if err != nil {
  458. return nil, err
  459. }
  460. outRefundNo := strings.TrimSpace(req.RefundID)
  461. if outRefundNo == "" {
  462. outRefundNo = wxpayRefundID(req.OrderID, req.Amount)
  463. }
  464. if outRefundNo == "" {
  465. return nil, fmt.Errorf("wxpay query refund: missing refund id")
  466. }
  467. rs := refunddomestic.RefundsApiService{Client: c}
  468. res, _, err := rs.QueryByOutRefundNo(ctx, refunddomestic.QueryByOutRefundNoRequest{
  469. OutRefundNo: core.String(outRefundNo),
  470. })
  471. if err != nil {
  472. return nil, fmt.Errorf("wxpay query refund: %w", err)
  473. }
  474. status := payment.ProviderStatusPending
  475. if res != nil && res.Status != nil {
  476. switch *res.Status {
  477. case refunddomestic.STATUS_SUCCESS:
  478. status = payment.ProviderStatusSuccess
  479. case refunddomestic.STATUS_CLOSED, refunddomestic.STATUS_ABNORMAL:
  480. status = payment.ProviderStatusFailed
  481. default:
  482. status = payment.ProviderStatusPending
  483. }
  484. }
  485. return &payment.RefundResponse{RefundID: outRefundNo, Status: status}, nil
  486. }
  487. func wxpayRefundID(orderID, amount string) string {
  488. orderID = strings.TrimSpace(orderID)
  489. if orderID == "" {
  490. return ""
  491. }
  492. amount = strings.NewReplacer(".", "", "-", "").Replace(strings.TrimSpace(amount))
  493. if amount == "" {
  494. return orderID + "-refund"
  495. }
  496. return orderID + "-refund-" + amount
  497. }
  498. func (w *Wxpay) queryOrderTotalFen(ctx context.Context, c *core.Client, orderID string) (int64, error) {
  499. svc := native.NativeApiService{Client: c}
  500. tx, _, err := svc.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
  501. OutTradeNo: core.String(orderID), Mchid: core.String(w.config["mchId"]),
  502. })
  503. if err != nil {
  504. return 0, fmt.Errorf("wxpay refund query order: %w", err)
  505. }
  506. var tf int64
  507. if tx.Amount != nil && tx.Amount.Total != nil {
  508. tf = *tx.Amount.Total
  509. }
  510. return tf, nil
  511. }
  512. func (w *Wxpay) CancelPayment(ctx context.Context, tradeNo string) error {
  513. c, err := w.ensureClient()
  514. if err != nil {
  515. return err
  516. }
  517. svc := native.NativeApiService{Client: c}
  518. _, err = svc.CloseOrder(ctx, native.CloseOrderRequest{
  519. OutTradeNo: core.String(tradeNo), Mchid: core.String(w.config["mchId"]),
  520. })
  521. if err != nil {
  522. return fmt.Errorf("wxpay cancel payment: %w", err)
  523. }
  524. return nil
  525. }
  526. var (
  527. _ payment.Provider = (*Wxpay)(nil)
  528. _ payment.CancelableProvider = (*Wxpay)(nil)
  529. )