diff --git a/package/pay/taptap/sign.go b/package/pay/taptap/sign.go index 4a14c87..e57a467 100644 --- a/package/pay/taptap/sign.go +++ b/package/pay/taptap/sign.go @@ -1,60 +1,62 @@ package taptap import ( - "bytes" - "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" - "github.com/gogf/gf/v2/os/gtime" - "github.com/gogf/gf/v2/util/grand" "io" "net/http" "sort" - "strconv" "strings" ) type pTapTap struct { - Secret string `json:"secret" dc:"秘钥"` - OrderId string `json:"order_id" dc:"订单唯一 ID"` + Secret string `json:"secret" dc:"秘钥"` + //OrderId string `json:"order_id" dc:"订单唯一 ID"` ClientId string `json:"client_id" dc:"应用的 Client ID"` } -func New(orderId string) *pTapTap { +func New(clientId string, secret string) *pTapTap { return &pTapTap{ - Secret: "5AFEWnadBA0NgJK2mxeBLQEde0qyIefxLSc4XKHsx9AwkQRhxzkQ9DixsOkK6gcV", - ClientId: "mox88lbz43edfukdgk", - OrderId: orderId, + Secret: secret, + ClientId: clientId, + //OrderId: orderId, } } -func (p *pTapTap) Sign(url string, body []byte) (sign string, ts int64, nonce string, err error) { - //nolint:gosec - secret := p.Secret - //body := gjson.MustEncode(g.Map{}) - //body := []byte(`{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}`) - //url := "https://example.com/my-service/v1/my-method" - ts = gtime.Now().Unix() - nonce = grand.S(5) - method := "POST" - header := http.Header{ - "Content-Type": {"Content-Type: application/json; charset=utf-8"}, - "X-Tap-Ts": {strconv.FormatInt(ts, 10)}, - "X-Tap-Nonce": {nonce}, - } - ctx := context.Background() - req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(body)) - req.Header = header - sign, err = Sign(req, secret) - if err != nil { - panic(err) - } - req.Header.Set("X-Tap-Sign", sign) - return +// Sign signs the request. +func (p *pTapTap) Sign(req *http.Request, secret string) (string, error) { + //获取请求参数 + //req := g.RequestFromCtx(ctx).Request + return Sign(req, secret) } +//func (p *pTapTap) SignOld(ctx context.Context, method, url string, token string, data any) (sign string, ts int64, nonce string, err error) { +// //secret := p.Secret +// //ts = gtime.Now().Unix() +// //nonce = grand.S(5) +// //header := http.Header{ +// // "Content-Type": {"Content-Type: application/json; charset=utf-8"}, +// // "X-Tap-Ts": {strconv.FormatInt(ts, 10)}, +// // "X-Tap-Nonce": {nonce}, +// //} +// //if method == "POST" { +// // header.Set("Content-Type", "application/json; charset=utf-8") +// //} +// ////ctx := context.Background() +// //request := g.RequestFromCtx(ctx).Request +// //body, _ := json.Marshal(data) +// ////req, err := http.NewRequestWithContext(ctx, method, url, strings.NewReader(string(body))) +// //req.Header = header +// //sign, err = Sign(req, secret) +// //if err != nil { +// // panic(err) +// //} +// //req.Header.Set("X-Tap-Sign", sign) +// //return +//} + // Sign signs the request. func Sign(req *http.Request, secret string) (string, error) { methodPart := req.Method diff --git a/package/pay/taptap/taptap.go b/package/pay/taptap/taptap.go index 6b9db46..a7fbfdf 100644 --- a/package/pay/taptap/taptap.go +++ b/package/pay/taptap/taptap.go @@ -1,59 +1,129 @@ package taptap import ( + "bytes" + "context" + "encoding/json" + "errors" "fmt" + "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gclient" "github.com/gogf/gf/v2/os/gctx" - "strconv" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/grand" + "io" + "net/http" ) type GetPayData struct { Data struct { Order struct { + Order } `json:"order"` } `json:"data"` Success bool `json:"success"` } -//查询订单信息 -func (p *pTapTap) Info(orderId string, clientId string, token []byte) (res string, err error) { - url := fmt.Sprintf("https://cloud-payment.tapapis.cn/order/v1/info?client_id=%v&order_id=%v", orderId, clientId) - res, err = p.get(url, token) - return +type Order struct { + OrderID string `json:"order_id"` // 订单唯一 ID + PurchaseToken string `json:"purchase_token"` // 用于订单核销的 token + ClientID string `json:"client_id"` // 应用的 Client ID + OpenID string `json:"open_id"` // 用户的开放平台 ID + UserRegion string `json:"user_region"` // 用户地区 + GoodsOpenID string `json:"goods_open_id"` // 商品唯一 ID + GoodsName string `json:"goods_name"` // 商品名称 + Status PaymentStatus `json:"status"` // 订单状态 + Amount string `json:"amount"` // 金额(本币金额 x 1,000,000) + Currency string `json:"currency"` // 币种 + CreateTime string `json:"create_time"` // 创建时间 + PayTime string `json:"pay_time"` // 支付时间 + Extra string `json:"extra"` // 商户自定义数据,如角色信息等,长度不超过 255 UTF-8 字符 } +type PaymentStatus string -//验证并核销订单 -func (p *pTapTap) Verify(orderId string, clientId string, token []byte) (res string, err error) { - url := fmt.Sprintf("https://cloud-payment.tapapis.cn/order/v1/verify?client_id=%v", clientId) - res, err = p.get(url, token) - return -} +const ( + ChargePending PaymentStatus = "charge.pending" // 待支付 + ChargeSucceeded PaymentStatus = "charge.succeeded" //支付成功 + ChargeConfirmed PaymentStatus = "charge.confirmed" //已核销 + ChargeOverdue PaymentStatus = "charge.overdue" //支付超时关闭 + RefundPending PaymentStatus = "refund.pending" //退款中 + RefundSucceeded PaymentStatus = "refund.succeeded" //退款成功 + RefundFailed PaymentStatus = "refund.failed" //退款失败 + RefundRejected PaymentStatus = "refund.rejected" //退款被拒绝 +) -func (p *pTapTap) get(url string, token []byte, _data ...any) (res string, err error) { - sign, ts, nonce, err := p.Sign(url, token) +// 查询订单信息 +func (p *pTapTap) Info(ctx context.Context, order string) (getPayData *GetPayData, err error) { + url := fmt.Sprintf("https://cloud-payment.tapapis.cn/order/v1/info?client_id=%v&order_id=%v", p.ClientId, order) + getPayData, err = p.get(ctx, url) if err != nil { return } + return +} + +// 验证并核销订单 +func (p *pTapTap) Verify(ctx context.Context, req any) (getPayData *GetPayData, err error) { + url := fmt.Sprintf("https://cloud-payment.tapapis.cn/order/v1/verify?client_id=%v", p.ClientId) + getPayData, err = p.get(ctx, url, req) + if err != nil { + return + } + return +} + +func (p *pTapTap) get(ctx context.Context, url string, _data ...any) (getPayData *GetPayData, err error) { + var _get *gclient.Response - if len(_data) == 0 { - _get, err = g.Client().Header(map[string]string{ - "X-Tap-Sign": sign, - "X-Tap-Nonce": nonce, - "X-Tap-Ts": strconv.FormatInt(ts, 10), - }).Get(gctx.New(), url) + var header = map[string]string{ + "Content-Type": "Content-Type: application/json; charset=utf-8", + "X-Tap-Nonce": grand.S(6), + "X-Tap-Ts": gtime.Now().TimestampStr(), + } + ctx2 := context.Background() + var method = "GET" + if len(_data) > 0 { + method = "POST" + + } + + //temp := []byte(`{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}`) + var body io.Reader + if len(_data) > 0 { + body = bytes.NewBuffer(gjson.MustEncode(_data[0])) } else { - _get, err = g.Client().Header(map[string]string{ - "X-Tap-Sign": sign, - "X-Tap-Nonce": nonce, - "X-Tap-Ts": strconv.FormatInt(ts, 10), - }).Post(gctx.New(), url, _data[0]) + body = bytes.NewBuffer([]byte{}) + } + req, _ := http.NewRequestWithContext(ctx2, method, url, body) + for k, v := range header { + req.Header.Set(k, v) + } + sign, err2 := p.Sign(req, p.Secret) + if err2 != nil { + err = err2 + return + } + req.Header.Set("X-Tap-Sign", sign) + header["X-Tap-Sign"] = sign + if len(_data) == 0 { + _get, err = g.Client().Header(header).ContentJson().Get(gctx.New(), url) + } else { + _get, err = g.Client().Header(header).ContentJson().Post(gctx.New(), url, _data[0]) } if err != nil { return } - res = _get.ReadAllString() + getPayData = &GetPayData{} + resData := _get.ReadAll() + g.Dump(resData) + if err = json.Unmarshal(resData, &getPayData); err != nil { + return + } + if !getPayData.Success { + err = errors.New(string(resData)) + } return } diff --git a/package/pay/taptap/webhook.go b/package/pay/taptap/webhook.go deleted file mode 100644 index 16b6b4f..0000000 --- a/package/pay/taptap/webhook.go +++ /dev/null @@ -1,40 +0,0 @@ -package taptap - -import "github.com/gogf/gf/v2/encoding/gjson" - -type WebhookData struct { - Order *Order `json:"order"` - EventType string `json:"event_type"` -} - -// Order 订单信息结构体 -type Order struct { - OrderID string `json:"order_id" dc:"订单唯一ID"` - PurchaseToken string `json:"purchase_token" dc:"用于订单核销的token"` - ClientID string `json:"client_id" dc:"应用的Client ID"` - OpenID string `json:"open_id" dc:"用户的开放平台ID"` - UserRegion string `json:"user_region" dc:"用户地区"` - GoodsOpenID string `json:"goods_open_id" dc:"商品唯一ID"` - GoodsName string `json:"goods_name" dc:"商品名称"` - Status string `json:"status" dc:"订单状态"` - Amount string `json:"amount" dc:"金额(本币金额x1,000,000)"` - Currency string `json:"currency" dc:"币种"` - CreateTime string `json:"create_time" dc:"创建时间"` - PayTime string `json:"pay_time" dc:"支付时间"` - Extra string `json:"extra" dc:"商户自定义数据,如角色信息等,长度不超过255 UTF-8字符"` -} - -func (p *pTapTap) Webhook(body []byte) (res string, err error) { - var data *WebhookData - gjson.DecodeTo(body, &data) - - switch data.EventType { - case "charge.succeeded": //充值成功 - //todo 处理订单信息 - - case "refund.succeeded": //退款成功 - case "refund.failed": //退款失败 - } - - return -} diff --git a/package/pay/xiaomi/helper.go b/package/pay/xiaomi/helper.go new file mode 100644 index 0000000..89a05e0 --- /dev/null +++ b/package/pay/xiaomi/helper.go @@ -0,0 +1,63 @@ +package xiaomi + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/hex" + "fmt" + "sort" + "strings" +) + +// SignatureHelper 签名辅助类 +type SignatureHelper struct{} + +// hmacSHA1 计算HMAC-SHA1哈希值 +func hmacSHA1(data, key string) string { + h := hmac.New(sha1.New, []byte(key)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +// Sign 计算hmac-sha1签名 +func (m *MiPay) Sign(params map[string]string, secretKey string) string { + if _, ok := params["signature"]; ok { + delete(params, "signature") + } + for k, v := range params { + if v == "" || v == "0" { + delete(params, k) + } + } + sortString := m.buildSortString(params) + signature := hmacSHA1(sortString, secretKey) + return signature +} + +// VerifySignature 验证签名 +func (m *MiPay) VerifySignature(params map[string]string, signature, secretKey string) bool { + tmpSign := m.Sign(params, secretKey) + return tmpSign == signature +} + +// buildSortString 构造排序字符串 +func (m *MiPay) buildSortString(params map[string]string) string { + if len(params) == 0 { + return "" + } + + // 按键排序 + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + // 构建排序字符串 + var fields []string + for _, k := range keys { + fields = append(fields, fmt.Sprintf("%s=%s", k, params[k])) + } + + return strings.Join(fields, "&") +} diff --git a/package/pay/xiaomi/xiaomi.go b/package/pay/xiaomi/xiaomi.go new file mode 100644 index 0000000..453c0b7 --- /dev/null +++ b/package/pay/xiaomi/xiaomi.go @@ -0,0 +1,29 @@ +package xiaomi + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gctx" +) + +// Config 小米支付配置信息 +type Config struct { + AppID string `json:"app_id"` // 应用ID + AppSecret string `json:"app_secret"` // 应用密钥 + //PrivateKey string // 商户私钥(如需证书) + //MIAPIURL string // 小米支付API基础地址 + //IsSandbox bool // 是否沙箱环境 +} + +// Miipay 小米支付客户端 +type MiPay struct { + config *Config +} + +func New() *MiPay { + _cfg, _ := g.Cfg().Get(gctx.New(), "pay.xiaomi") + var cfg *Config + _cfg.Scan(&cfg) + return &MiPay{ + config: cfg, + } +}