141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
// Package epay implements the 易支付 mapi (API 下单) interface with MD5 signing.
|
|
// POST {api_base}/mapi.php → JSON {code, msg, trade_no, payurl, qrcode}.
|
|
// Docs: 请求字段 pid/type/out_trade_no/notify_url/return_url/name/money/sign/sign_type.
|
|
package epay
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
APIBase string // 易支付站点根地址,如 https://www.ezfpy.cn(自动拼 /mapi.php)
|
|
PID string // 商户ID
|
|
Key string // 商户密钥
|
|
}
|
|
|
|
type CreateRequest struct {
|
|
OutTradeNo string
|
|
Type string // wxpay | alipay | unionpay
|
|
Name string
|
|
Money string // "10.00"
|
|
NotifyURL string
|
|
ReturnURL string // 页面跳转通知地址(必填)
|
|
ClientIP string // unused by mapi; kept for caller convenience
|
|
}
|
|
|
|
type CreateResult struct {
|
|
TradeNo string // 平台订单号
|
|
PayType string // qrcode | jump
|
|
PayInfo string // 二维码内容 或 跳转 url
|
|
}
|
|
|
|
// mapiResp is the raw mapi response. code: 1 或 200 为成功,其它为失败.
|
|
type mapiResp struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
TradeNo string `json:"trade_no"`
|
|
PayURL string `json:"payurl"`
|
|
QRCode string `json:"qrcode"`
|
|
}
|
|
|
|
var httpClient = &http.Client{Timeout: 20 * time.Second}
|
|
|
|
// sign builds the MD5 signature: take all params except sign/sign_type and empty
|
|
// values, sort keys ASCII-ascending, join as k=v&k=v (raw values), append the
|
|
// merchant key, MD5, lowercase hex.
|
|
func sign(params map[string]string, key string) string {
|
|
keys := make([]string, 0, len(params))
|
|
for k, v := range params {
|
|
if k == "sign" || k == "sign_type" || v == "" {
|
|
continue
|
|
}
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
var b strings.Builder
|
|
for i, k := range keys {
|
|
if i > 0 {
|
|
b.WriteByte('&')
|
|
}
|
|
b.WriteString(k)
|
|
b.WriteByte('=')
|
|
b.WriteString(params[k])
|
|
}
|
|
b.WriteString(key)
|
|
sum := md5.Sum([]byte(b.String()))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// Create places a mapi order and returns the payment info (qrcode preferred).
|
|
func (c *Config) Create(ctx context.Context, req CreateRequest) (*CreateResult, error) {
|
|
params := map[string]string{
|
|
"pid": c.PID,
|
|
"type": req.Type,
|
|
"out_trade_no": req.OutTradeNo,
|
|
"notify_url": req.NotifyURL,
|
|
"name": req.Name,
|
|
"money": req.Money,
|
|
"return_url": req.ReturnURL,
|
|
"sign_type": "MD5",
|
|
}
|
|
params["sign"] = sign(params, c.Key)
|
|
|
|
form := url.Values{}
|
|
for k, v := range params {
|
|
form.Set(k, v)
|
|
}
|
|
endpoint := strings.TrimRight(c.APIBase, "/") + "/mapi.php"
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
resp, err := httpClient.Do(httpReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
var out mapiResp
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return nil, fmt.Errorf("epay: bad response: %s", strings.TrimSpace(string(body)))
|
|
}
|
|
if out.Code != 1 && out.Code != 200 {
|
|
msg := out.Msg
|
|
if msg == "" {
|
|
msg = "下单失败"
|
|
}
|
|
return nil, fmt.Errorf("epay: %s", msg)
|
|
}
|
|
result := &CreateResult{TradeNo: out.TradeNo}
|
|
if out.QRCode != "" {
|
|
result.PayType = "qrcode"
|
|
result.PayInfo = out.QRCode
|
|
} else if out.PayURL != "" {
|
|
result.PayType = "jump"
|
|
result.PayInfo = out.PayURL
|
|
} else {
|
|
return nil, fmt.Errorf("epay: 响应缺少 qrcode/payurl")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// VerifyNotify validates an async-notify callback's MD5 signature.
|
|
func (c *Config) VerifyNotify(params map[string]string) bool {
|
|
got := strings.TrimSpace(params["sign"])
|
|
if got == "" {
|
|
return false
|
|
}
|
|
return strings.EqualFold(got, sign(params, c.Key))
|
|
}
|