2026-06-11 01:02:16 +08:00

269 lines
8.1 KiB
Go

package mifapay
import (
"bytes"
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/config"
"hyapp/services/wallet-service/internal/domain/ledger"
walletservice "hyapp/services/wallet-service/internal/service/wallet"
)
type Client struct {
cfg config.MifaPayConfig
httpClient *http.Client
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
}
type envelope struct {
MerAccount string `json:"merAccount"`
Data string `json:"data"`
Sign string `json:"sign"`
}
func New(cfg config.MifaPayConfig) (*Client, error) {
privateKey, err := parsePrivateKey(cfg.PrivateKey)
if err != nil {
return nil, err
}
publicKey, err := parsePublicKey(cfg.PlatformPublicKey)
if err != nil {
return nil, err
}
if cfg.HTTPTimeout <= 0 {
cfg.HTTPTimeout = 10 * time.Second
}
return &Client{
cfg: cfg,
httpClient: &http.Client{Timeout: cfg.HTTPTimeout},
privateKey: privateKey,
publicKey: publicKey,
}, nil
}
// CreateOrder 按 MiFaPay 统一 envelope 协议下单;业务层只消费 payUrl 和 mbOrderId。
func (c *Client) CreateOrder(ctx context.Context, req walletservice.MifaPayCreateOrderRequest) (walletservice.MifaPayCreateOrderResponse, error) {
if c == nil || c.privateKey == nil {
return walletservice.MifaPayCreateOrderResponse{}, xerr.New(xerr.Unavailable, "mifapay client is not configured")
}
data := map[string]any{
"amount": strconv.FormatInt(req.ProviderAmountMinor, 10),
"orderId": req.OrderID,
"payWay": req.PayWay,
"language": firstNonEmpty(req.Language, "en"),
"terminalType": "WEB",
"productInfo": productInfo(req),
"payType": req.PayType,
"merNo": c.cfg.MerNo,
"countryCode": req.CountryCode,
"notifyUrl": req.NotifyURL,
"currency": req.CurrencyCode,
"time": time.Now().Unix(),
"returnUrl": req.ReturnURL,
"memberId": strconv.FormatInt(req.TargetUserID, 10),
"userIp": req.ClientIP,
}
body, err := c.signedEnvelope(data)
if err != nil {
return walletservice.MifaPayCreateOrderResponse{}, err
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.APIBaseURL+"/paygateway/mbpay/order/en_v2", bytes.NewReader(body))
if err != nil {
return walletservice.MifaPayCreateOrderResponse{}, err
}
httpReq.Header.Set("Content-Type", "application/json; charset=UTF-8")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return walletservice.MifaPayCreateOrderResponse{}, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
if err != nil {
return walletservice.MifaPayCreateOrderResponse{}, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return walletservice.MifaPayCreateOrderResponse{}, xerr.New(xerr.Unavailable, "mifapay order request failed")
}
var outer envelope
var withCode struct {
envelope
Code string `json:"code"`
Msg string `json:"msg"`
}
if err := json.Unmarshal(raw, &withCode); err != nil {
return walletservice.MifaPayCreateOrderResponse{}, err
}
outer = withCode.envelope
if withCode.Code != "" && withCode.Code != "000000" {
return walletservice.MifaPayCreateOrderResponse{}, xerr.New(xerr.Conflict, "mifapay order rejected: "+withCode.Msg)
}
if err := c.verify(outer.Data, outer.Sign); err != nil {
return walletservice.MifaPayCreateOrderResponse{}, err
}
var inner struct {
OrderID string `json:"orderId"`
PayURL string `json:"payUrl"`
MBOrderID string `json:"mbOrderId"`
}
if err := json.Unmarshal([]byte(outer.Data), &inner); err != nil {
return walletservice.MifaPayCreateOrderResponse{}, err
}
if strings.TrimSpace(inner.PayURL) == "" || strings.TrimSpace(inner.MBOrderID) == "" {
return walletservice.MifaPayCreateOrderResponse{}, xerr.New(xerr.Unavailable, "mifapay order response is incomplete")
}
return walletservice.MifaPayCreateOrderResponse{
OrderID: inner.OrderID,
ProviderOrderID: inner.MBOrderID,
PayURL: inner.PayURL,
RawJSON: string(raw),
}, nil
}
// ParseNotification 验签 MiFaPay 回调并返回内层业务字段;验签失败不允许业务层入账。
func (c *Client) ParseNotification(notification ledger.MifaPayNotification) (walletservice.MifaPayNotifyData, error) {
if c == nil || c.publicKey == nil {
return walletservice.MifaPayNotifyData{}, xerr.New(xerr.Unavailable, "mifapay client is not configured")
}
if strings.TrimSpace(notification.MerAccount) != c.cfg.MerAccount {
return walletservice.MifaPayNotifyData{}, xerr.New(xerr.PermissionDenied, "mifapay mer_account is invalid")
}
if err := c.verify(notification.Data, notification.Sign); err != nil {
return walletservice.MifaPayNotifyData{}, err
}
var inner struct {
Amount string `json:"amount"`
PayType string `json:"payType"`
OrderID string `json:"orderId"`
BankOrderNo string `json:"bankOrderNo"`
OrderStatus string `json:"orderStatus"`
Currency string `json:"currency"`
MBOrderID string `json:"mbOrderId"`
MemberID string `json:"memberId"`
}
if err := json.Unmarshal([]byte(notification.Data), &inner); err != nil {
return walletservice.MifaPayNotifyData{}, err
}
if strings.TrimSpace(inner.OrderID) == "" {
return walletservice.MifaPayNotifyData{}, xerr.New(xerr.InvalidArgument, "mifapay callback order_id is required")
}
return walletservice.MifaPayNotifyData{
OrderID: inner.OrderID,
ProviderOrderID: inner.MBOrderID,
OrderStatus: inner.OrderStatus,
Amount: inner.Amount,
Currency: inner.Currency,
PayType: inner.PayType,
RawJSON: notification.Data,
}, nil
}
func (c *Client) signedEnvelope(data any) ([]byte, error) {
dataJSON, err := json.Marshal(data)
if err != nil {
return nil, err
}
sign, err := c.sign(string(dataJSON))
if err != nil {
return nil, err
}
return json.Marshal(envelope{MerAccount: c.cfg.MerAccount, Data: string(dataJSON), Sign: sign})
}
func (c *Client) sign(data string) (string, error) {
hash := sha256.Sum256([]byte(data))
signature, err := rsa.SignPKCS1v15(rand.Reader, c.privateKey, crypto.SHA256, hash[:])
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
func (c *Client) verify(data string, sign string) error {
if c.publicKey == nil {
return nil
}
signature, err := base64.StdEncoding.DecodeString(strings.TrimSpace(sign))
if err != nil {
return err
}
hash := sha256.Sum256([]byte(data))
if err := rsa.VerifyPKCS1v15(c.publicKey, crypto.SHA256, hash[:], signature); err != nil {
return xerr.New(xerr.PermissionDenied, "mifapay signature is invalid")
}
return nil
}
func parsePrivateKey(value string) (*rsa.PrivateKey, error) {
block := pemBlock(value, "PRIVATE KEY")
key, err := x509.ParsePKCS8PrivateKey(block)
if err != nil {
return nil, err
}
privateKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("mifapay private key is not rsa")
}
return privateKey, nil
}
func parsePublicKey(value string) (*rsa.PublicKey, error) {
block := pemBlock(value, "PUBLIC KEY")
key, err := x509.ParsePKIXPublicKey(block)
if err != nil {
return nil, err
}
publicKey, ok := key.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("mifapay public key is not rsa")
}
return publicKey, nil
}
func pemBlock(value string, blockType string) []byte {
value = strings.TrimSpace(value)
if decoded, _ := pem.Decode([]byte(value)); decoded != nil {
return decoded.Bytes
}
clean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(value)
body, err := base64.StdEncoding.DecodeString(clean)
if err == nil {
return body
}
return []byte(value)
}
func productInfo(req walletservice.MifaPayCreateOrderRequest) string {
body, _ := json.Marshal([]map[string]string{{
"quantity": "1",
"price": strconv.FormatInt(req.AmountMinor, 10),
"sku": req.OrderID,
"productName": req.ProductName,
}})
return string(body)
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
return ""
}